mirror of
https://github.com/abusoww/tuxmate.git
synced 2026-04-26 18:35:12 +02:00
refined UI/UX (new fonts, focus states, tooltips)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||
import { type Category } from '@/lib/data';
|
||||
|
||||
// What we're navigating to
|
||||
@@ -25,6 +25,12 @@ export function useKeyboardNavigation(
|
||||
) {
|
||||
const [focusPos, setFocusPos] = useState<FocusPosition | null>(null);
|
||||
|
||||
// Track if focus was set via keyboard (to enable scroll) vs mouse (no scroll)
|
||||
const fromKeyboard = useRef(false);
|
||||
|
||||
// Track if focus mode is keyboard (for UI highlighting)
|
||||
const [isKeyboardNavigating, setIsKeyboardNavigating] = useState(false);
|
||||
|
||||
/** Clear focus (e.g., when clicking outside) */
|
||||
const clearFocus = useCallback(() => setFocusPos(null), []);
|
||||
|
||||
@@ -34,12 +40,14 @@ export function useKeyboardNavigation(
|
||||
return navItems[focusPos.col]?.[focusPos.row] || null;
|
||||
}, [navItems, focusPos]);
|
||||
|
||||
/** Set focus position by item type and id */
|
||||
/** Set focus position by item type and id (from mouse - no scroll) */
|
||||
const setFocusByItem = useCallback((type: 'category' | 'app', id: string) => {
|
||||
for (let col = 0; col < navItems.length; col++) {
|
||||
const colItems = navItems[col];
|
||||
for (let row = 0; row < colItems.length; row++) {
|
||||
if (colItems[row].type === type && colItems[row].id === id) {
|
||||
fromKeyboard.current = false; // Mouse selection - don't scroll
|
||||
setIsKeyboardNavigating(false); // Disable focus ring
|
||||
setFocusPos({ col, row });
|
||||
return;
|
||||
}
|
||||
@@ -79,6 +87,10 @@ export function useKeyboardNavigation(
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark as keyboard navigation - will trigger scroll and focus ring
|
||||
fromKeyboard.current = true;
|
||||
setIsKeyboardNavigating(true);
|
||||
|
||||
// Navigate
|
||||
setFocusPos(prev => {
|
||||
if (!prev) return { col: 0, row: 0 };
|
||||
@@ -117,16 +129,18 @@ export function useKeyboardNavigation(
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [navItems, focusPos, onToggleCategory, onToggleApp]);
|
||||
|
||||
/* Scroll focused item into view instantly */
|
||||
/* Scroll focused item into view - only when navigating via keyboard */
|
||||
useEffect(() => {
|
||||
if (!focusPos) return;
|
||||
if (!focusPos || !fromKeyboard.current) return;
|
||||
|
||||
const item = navItems[focusPos.col]?.[focusPos.row];
|
||||
if (!item) return;
|
||||
|
||||
const el = document.querySelector<HTMLElement>(
|
||||
// Find visible element among duplicates (mobile/desktop layouts both render same data-nav-id)
|
||||
const elements = document.querySelectorAll<HTMLElement>(
|
||||
`[data-nav-id="${item.type}:${item.id}"]`
|
||||
);
|
||||
const el = Array.from(elements).find(e => e.offsetWidth > 0 && e.offsetHeight > 0);
|
||||
|
||||
if (!el) return;
|
||||
|
||||
@@ -142,5 +156,6 @@ export function useKeyboardNavigation(
|
||||
focusedItem,
|
||||
clearFocus,
|
||||
setFocusByItem,
|
||||
isKeyboardNavigating,
|
||||
};
|
||||
}
|
||||
|
||||
101
src/hooks/useTooltip.ts
Normal file
101
src/hooks/useTooltip.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
|
||||
export interface TooltipState {
|
||||
content: string;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tooltip that stays open while hovering trigger or tooltip.
|
||||
* - 450ms delay before showing
|
||||
* - Stays open once shown (until mouse leaves both trigger and tooltip)
|
||||
* - Dismiss on click/scroll/escape
|
||||
*/
|
||||
export function useTooltip() {
|
||||
const [tooltip, setTooltip] = useState<TooltipState | null>(null);
|
||||
const showTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
const hideTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
const isOverTrigger = useRef(false);
|
||||
const isOverTooltip = useRef(false);
|
||||
|
||||
const cancel = useCallback(() => {
|
||||
if (showTimeout.current) {
|
||||
clearTimeout(showTimeout.current);
|
||||
showTimeout.current = null;
|
||||
}
|
||||
if (hideTimeout.current) {
|
||||
clearTimeout(hideTimeout.current);
|
||||
hideTimeout.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const tryHide = useCallback(() => {
|
||||
cancel();
|
||||
// Only hide if mouse is not over trigger or tooltip
|
||||
hideTimeout.current = setTimeout(() => {
|
||||
if (!isOverTrigger.current && !isOverTooltip.current) {
|
||||
setTooltip(null);
|
||||
}
|
||||
}, 100);
|
||||
}, [cancel]);
|
||||
|
||||
const show = useCallback((content: string, e: React.MouseEvent) => {
|
||||
const target = e.currentTarget as HTMLElement;
|
||||
isOverTrigger.current = true;
|
||||
cancel();
|
||||
|
||||
const rect = target.getBoundingClientRect();
|
||||
|
||||
showTimeout.current = setTimeout(() => {
|
||||
setTooltip({
|
||||
content,
|
||||
x: rect.left + rect.width / 2,
|
||||
y: rect.top,
|
||||
});
|
||||
}, 450);
|
||||
}, [cancel]);
|
||||
|
||||
const hide = useCallback(() => {
|
||||
isOverTrigger.current = false;
|
||||
tryHide();
|
||||
}, [tryHide]);
|
||||
|
||||
const tooltipMouseEnter = useCallback(() => {
|
||||
isOverTooltip.current = true;
|
||||
cancel();
|
||||
}, [cancel]);
|
||||
|
||||
const tooltipMouseLeave = useCallback(() => {
|
||||
isOverTooltip.current = false;
|
||||
tryHide();
|
||||
}, [tryHide]);
|
||||
|
||||
useEffect(() => {
|
||||
const dismiss = () => {
|
||||
cancel();
|
||||
isOverTrigger.current = false;
|
||||
isOverTooltip.current = false;
|
||||
setTooltip(null);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') dismiss();
|
||||
};
|
||||
|
||||
window.addEventListener('mousedown', dismiss, true);
|
||||
window.addEventListener('scroll', dismiss, true);
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
cancel();
|
||||
window.removeEventListener('mousedown', dismiss, true);
|
||||
window.removeEventListener('scroll', dismiss, true);
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [cancel]);
|
||||
|
||||
return { tooltip, show, hide, tooltipMouseEnter, tooltipMouseLeave };
|
||||
}
|
||||
Reference in New Issue
Block a user