mirror of
https://github.com/abusoww/tuxmate.git
synced 2026-04-23 17:15:12 +02:00
refined UI/UX (new fonts, focus states, tooltips)
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user