feat: Tooltip portal with anchorToCursor mode

Tooltip now portals cursor-anchored variants into document.body via a
`mounted` guard, preventing transformed parents and scroll-container
overflow from distorting coordinates. New `anchorToCursor` prop locks
position at hover entry without tracking; `followCursor` retains the
spring-animated tracking behaviour. Color swatches (ColorFilter),
timeline scrubber dots/labels (Timeline), and toolbar dropdowns (Toolbar)
are updated to use the appropriate cursor mode. Toolbar z-index bumped
z-20→z-40 (dropdowns z-30→z-50) to layer above portaled content; tag
autocomplete result guard added (Array.isArray).
This commit is contained in:
2026-06-29 16:28:35 +01:00
parent d81624573d
commit 9144be2518
4 changed files with 81 additions and 68 deletions
+19 -18
View File
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } fr
import { useVirtualizer } from "@tanstack/react-virtual";
import { ImageRecord, tileSizeForZoom, useGalleryStore } from "../store";
import { ContextMenu, ImageTile } from "./Gallery";
import { Tooltip } from "./Tooltip";
const GAP = 6;
const HEADER_HEIGHT = 52;
@@ -374,16 +375,16 @@ function ScrubberYearBlock({ yearEntry, activeGroupIndex, onScrollTo }: Scrubber
return (
<div className="w-full flex flex-col items-center">
<button
className={`w-full text-center py-0.5 text-[10px] font-semibold tracking-wide transition-colors ${
isYearActive ? "text-white/80" : "text-white/30 hover:text-white/55"
}`}
onClick={() => onScrollTo(yearEntry.firstGroupIndex)}
title={yearEntry.year}
>
{yearEntry.year}
</button>
<Tooltip label={yearEntry.year} anchorToCursor>
<button
className={`w-full text-center py-0.5 text-[10px] font-semibold tracking-wide transition-colors ${
isYearActive ? "text-white/80" : "text-white/30 hover:text-white/55"
}`}
onClick={() => onScrollTo(yearEntry.firstGroupIndex)}
>
{yearEntry.year}
</button>
</Tooltip>
<div
className="grid gap-[3px] pb-1.5"
style={{ gridTemplateColumns: "repeat(3, 10px)" }}
@@ -396,14 +397,14 @@ function ScrubberYearBlock({ yearEntry, activeGroupIndex, onScrollTo }: Scrubber
return <span key={monthNum} className="h-[10px] w-[10px]" />;
}
return (
<button
key={monthNum}
title={`${monthEntry.label} ${yearEntry.year}`}
onClick={() => onScrollTo(monthEntry.groupIndex)}
className={`h-[10px] w-[10px] rounded-full transition-colors ${
isActive ? "bg-white/70" : "bg-white/15 hover:bg-white/40"
}`}
/>
<Tooltip key={monthNum} label={`${monthEntry.label} ${yearEntry.year}`} anchorToCursor>
<button
onClick={() => onScrollTo(monthEntry.groupIndex)}
className={`h-[10px] w-[10px] rounded-full transition-colors ${
isActive ? "bg-white/70" : "bg-white/15 hover:bg-white/40"
}`}
/>
</Tooltip>
);
})}
</div>