diff --git a/src/components/ColorFilter.tsx b/src/components/ColorFilter.tsx index f4bcbe9..1cca85b 100644 --- a/src/components/ColorFilter.tsx +++ b/src/components/ColorFilter.tsx @@ -56,7 +56,7 @@ export function ColorFilter() { }, [open]); return ( -
+
{/* Trigger — a single palette icon; shows the active color as a dot when a filter is applied so the collapsed state still communicates it. */} @@ -99,9 +99,9 @@ export function ColorFilter() { {SWATCHES.map((swatch) => { const active = rgbEquals(colorFilter, swatch.rgb); return ( +
{isActive || (colorBackfill && colorBackfill.total > 0) ? ( -
+
{colorBackfill && colorBackfill.total > 0 ? ( - - + + +
; } return ( -
diff --git a/src/components/Toolbar.tsx b/src/components/Toolbar.tsx index f2595fa..90ef087 100644 --- a/src/components/Toolbar.tsx +++ b/src/components/Toolbar.tsx @@ -3,6 +3,7 @@ import { invoke } from "@tauri-apps/api/core"; import { tileSizeForZoom, useGalleryStore, SortOrder, MediaFilter, SearchCommand, parseSearchValue, searchModeLabel, ExploreTagEntry } from "../store"; import { FolderScopeDropdown } from "./FolderScopeDropdown"; import { ColorFilter } from "./ColorFilter"; +import { Tooltip } from "./Tooltip"; const BASE_SORT_OPTIONS: { value: SortOrder; label: string }[] = [ { value: "date_desc", label: "Newest first" }, @@ -230,7 +231,7 @@ export function Toolbar() { const results = await invoke("search_tags_autocomplete", { params: { query: searchQuery.trim(), folder_id: selectedFolderId ?? null, limit: 10 }, }); - setTagSuggestions(results); + setTagSuggestions(Array.isArray(results) ? results : []); } catch { setTagSuggestions([]); } @@ -266,7 +267,7 @@ export function Toolbar() { const showCommandHints = !searchCommand && searchPanelOpen; return ( -
+
{/* Primary row */}
{/* Title + count */} @@ -357,7 +358,7 @@ export function Toolbar() { {/* Tag autocomplete suggestions */} {showTagSuggestions && tagSuggestions.length > 0 ? ( -
+
{tagSuggestions.map((entry) => ( + + + ))}
diff --git a/src/components/Tooltip.tsx b/src/components/Tooltip.tsx index c345c4a..6aee866 100644 --- a/src/components/Tooltip.tsx +++ b/src/components/Tooltip.tsx @@ -1,4 +1,5 @@ import { MouseEvent, ReactNode, useEffect, useRef, useState } from "react"; +import { createPortal } from "react-dom"; import { motion } from "framer-motion"; type TooltipSide = "top" | "bottom" | "left" | "right"; @@ -20,8 +21,8 @@ const STATIC_CLASSES = `${BASE_CLASSES} transition-opacity duration-100`; /** * Lightweight custom tooltip — fades in (faster than the native one) after a * tunable hover `delay`, and hides instantly on leave or click. By default it - * anchors to a `side` of the wrapper; with `followCursor` it tracks the pointer - * (fixed-positioned, so it escapes scroll-container clipping). + * anchors to a `side` of the wrapper. `anchorToCursor` shows at the pointer's + * entry position; `followCursor` keeps tracking the pointer. */ export function Tooltip({ label, @@ -29,6 +30,7 @@ export function Tooltip({ side = "bottom", align = "center", block = false, + anchorToCursor = false, followCursor = false, className = "", children, @@ -41,6 +43,8 @@ export function Tooltip({ align?: TooltipAlign; /** Full-width block wrapper (e.g. wrapping a grid cell) instead of inline. */ block?: boolean; + /** Position at the cursor when shown, without following subsequent movement. */ + anchorToCursor?: boolean; /** Track the cursor (fixed position) instead of anchoring to a side. */ followCursor?: boolean; /** Extra classes for the wrapper (e.g. layout). */ @@ -49,6 +53,7 @@ export function Tooltip({ }) { const [visible, setVisible] = useState(false); const [coords, setCoords] = useState({ x: 0, y: 0 }); + const [mounted, setMounted] = useState(false); const timer = useRef | undefined>(undefined); const frame = useRef(undefined); const tooltipRef = useRef(null); @@ -79,7 +84,7 @@ export function Tooltip({ }; const show = (event: MouseEvent) => { clear(); - if (followCursor) place(event.clientX, event.clientY); + if (anchorToCursor || followCursor) place(event.clientX, event.clientY); if (delay <= 0) { setVisible(true); return; @@ -100,9 +105,31 @@ export function Tooltip({ }); }; - useEffect(() => clear, []); + useEffect(() => { + setMounted(true); + return clear; + }, []); const Wrapper = block ? "div" : "span"; + const cursorTooltip = ( + + {label} + + ); return ( {children} - {followCursor ? ( - - {label} - + {anchorToCursor || followCursor ? ( + mounted ? createPortal(cursorTooltip, document.body) : null ) : (