refined UI/UX (new fonts, focus states, tooltips)

This commit is contained in:
N1C4T
2026-01-06 21:21:44 +04:00
parent 6c9b2123dd
commit 6d4fcca6c5
14 changed files with 270 additions and 472 deletions

View File

@@ -1,62 +1,56 @@
'use client';
import React from 'react';
export interface TooltipData {
text: string;
x: number;
y: number;
width: number;
key: number;
}
import React, { useState, useEffect, useRef } from 'react';
import { type TooltipState } from '@/hooks/useTooltip';
interface TooltipProps {
tooltip: TooltipData | null;
onEnter?: () => void;
onLeave?: () => void;
tooltip: TooltipState | null;
onMouseEnter: () => void;
onMouseLeave: () => void;
}
// Floating tooltip with markdown rendering - follows the cursor around
export function Tooltip({ tooltip, onEnter, onLeave }: TooltipProps) {
if (!tooltip) return null;
export function Tooltip({ tooltip, onMouseEnter, onMouseLeave }: TooltipProps) {
const [current, setCurrent] = useState<TooltipState | null>(null);
const [visible, setVisible] = useState(false);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
// Center horizontally relative to the element
const left = tooltip.x;
const top = tooltip.y;
useEffect(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
if (tooltip) {
// eslint-disable-next-line
setCurrent(tooltip);
requestAnimationFrame(() => setVisible(true));
} else {
setVisible(false);
timeoutRef.current = setTimeout(() => setCurrent(null), 60);
}
return () => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
};
}, [tooltip]);
if (!current) return null;
// Helper to render markdown content
const renderContent = (text: string) => {
// Split by **bold**, `code`, or [link](url)
return text.split(/(\*\*.*?\*\*|`.*?`|\[.*?\]\(.*?\))/g).map((part, i) => {
// Bold
return text.split(/(\*\*[^*]+\*\*|`[^`]+`|\[[^\]]+\]\([^)]+\))/g).map((part, i) => {
if (part.startsWith('**') && part.endsWith('**')) {
return (
<strong key={i} className="font-bold text-[var(--accent)]">
{part.slice(2, -2)}
</strong>
);
return <strong key={i} className="font-medium text-[var(--text-primary)]">{part.slice(2, -2)}</strong>;
}
// Code
if (part.startsWith('`') && part.endsWith('`')) {
return (
<code key={i} className="bg-[var(--bg-secondary)] px-1 rounded font-mono text-[var(--accent)] text-[10px]">
{part.slice(1, -1)}
</code>
);
return <code key={i} className="px-1 py-0.5 rounded bg-black/20 font-mono text-[var(--accent)] text-[11px]">{part.slice(1, -1)}</code>;
}
// Link
if (part.startsWith('[') && part.includes('](') && part.endsWith(')')) {
const match = part.match(/\[(.*?)\]\((.*?)\)/);
if (match) {
return (
<a
key={i}
href={match[2]}
target="_blank"
rel="noopener noreferrer"
className="text-[var(--accent)] underline decoration-[var(--accent)]/50 hover:decoration-[var(--accent)] font-semibold transition-colors hover:text-emerald-500"
onClick={(e) => e.stopPropagation()} // Prevent triggering parent clicks
>
<a key={i} href={match[2]} target="_blank" rel="noopener noreferrer"
className="text-[var(--accent)] underline underline-offset-2 hover:brightness-125 transition-all"
onClick={(e) => e.stopPropagation()}>
{match[1]}
</a>
);
@@ -66,30 +60,45 @@ export function Tooltip({ tooltip, onEnter, onLeave }: TooltipProps) {
});
};
// Hide tooltips on mobile - they don't work with touch
return (
<div
role="tooltip"
className="hidden md:block fixed z-50 pointer-events-auto"
style={{
left: left,
top: top,
transform: 'translate(-50%, -100%)',
// Using the specific key ensures fresh animation on new tooltip
animation: 'tooltipSlideUp 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards',
maxWidth: '400px', // Limit width
width: 'max-content'
}}
onMouseEnter={onEnter}
onMouseLeave={onLeave}
className="fixed hidden md:block pointer-events-auto z-[9999]"
style={{ left: current.x, top: current.y - 10 }}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<div className="relative mb-2 px-3 py-2 rounded-lg bg-[var(--bg-tertiary)] text-[var(--text-primary)] text-xs font-medium shadow-xl border border-[var(--border-primary)]/40 backdrop-blur-sm whitespace-normal break-words leading-relaxed">
{renderContent(tooltip.text)}
{/* Arrow pointer */}
<div className={`
absolute left-1/2 bottom-0 -translate-x-1/2
transition-opacity duration-75
${visible ? 'opacity-100' : 'opacity-0 pointer-events-none'}
`}>
{/* Clean tooltip bubble */}
<div
className="absolute -bottom-1 left-1/2 -translate-x-1/2 w-2 h-2 bg-[var(--bg-tertiary)] border-b border-r border-[var(--border-primary)]/40 rotate-45"
/>
className="px-3.5 py-2.5 rounded-lg shadow-lg overflow-hidden"
style={{
minWidth: '300px',
maxWidth: '300px',
backgroundColor: 'var(--bg-secondary)',
border: '1px solid var(--border-primary)',
}}
>
<p className="text-[13px] leading-[1.55] text-[var(--text-secondary)] break-words" style={{ wordBreak: 'break-word' }}>
{renderContent(current.content)}
</p>
</div>
{/* Arrow */}
<div className="absolute left-1/2 -translate-x-1/2 -bottom-[5px]">
<div
className="w-2.5 h-2.5 rotate-45"
style={{
backgroundColor: 'var(--bg-secondary)',
borderRight: '1px solid var(--border-primary)',
borderBottom: '1px solid var(--border-primary)',
}}
/>
</div>
</div>
</div>
);