diff --git a/src/components/BackgroundTasks.tsx b/src/components/BackgroundTasks.tsx index 4040d79..aa85224 100644 --- a/src/components/BackgroundTasks.tsx +++ b/src/components/BackgroundTasks.tsx @@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from "react"; import { invoke } from "@tauri-apps/api/core"; import { revealItemInDir } from "@tauri-apps/plugin-opener"; import { useGalleryStore, WorkerKey } from "../store"; +import { Tooltip } from "./Tooltip"; const WORKER_FOR_STAGE: Record = { Thumbnails: "thumbnail", @@ -52,15 +53,16 @@ function FailedWorkerItemRow({ item }: { item: FailedWorkerItem }) {

{item.error}

)} + + ); } @@ -378,9 +380,9 @@ export function BackgroundTasks() { {stage.detail} {workerKey && ( + - )} + + )} ); })} @@ -455,16 +458,17 @@ export function BackgroundTasks() { {/* Dismiss — hidden for system tasks like duplicate scan */} {primary.id >= 0 && ( + - )} + + )} {/* Expanded panel — one row per folder */} @@ -508,9 +512,9 @@ export function BackgroundTasks() { {stage.detail} {workerKey && ( + - )} + + )} ); })} @@ -565,15 +570,16 @@ export function BackgroundTasks() { )} {task.id >= 0 && ( + + )} diff --git a/src/components/BulkActionBar.tsx b/src/components/BulkActionBar.tsx index 3404f24..328b175 100644 --- a/src/components/BulkActionBar.tsx +++ b/src/components/BulkActionBar.tsx @@ -1,6 +1,7 @@ import { useEffect, useRef, useState } from "react"; import { useGalleryStore } from "../store"; import { BulkTagPopover } from "./bulk/BulkTagPopover"; +import { Tooltip } from "./Tooltip" type Panel = "tag" | "rating" | "album" | "delete" | null; @@ -93,14 +94,15 @@ export function BulkActionBar() {
{selectedCount} selected {loadedCount < totalImages || loadedCount > selectedCount ? ( + - ) : null} + + ) : null}
@@ -124,6 +126,7 @@ export function BulkActionBar() { {Array.from({ length: 5 }, (_, index) => { const rating = index + 1; return ( + + ); })}
- - - + + +
+ {panel === "delete" ? (
) : null}
- + +
); } diff --git a/src/components/ColorFilter.tsx b/src/components/ColorFilter.tsx index 1cca85b..200ed7e 100644 --- a/src/components/ColorFilter.tsx +++ b/src/components/ColorFilter.tsx @@ -59,7 +59,7 @@ export function ColorFilter() {
{/* Trigger — a single palette icon; shows the active color as a dot when a filter is applied so the collapsed state still communicates it. */} - +
{isActive || (colorBackfill && colorBackfill.total > 0) ? (
{colorBackfill && colorBackfill.total > 0 ? ( - - sampling {colorBackfill.processed.toLocaleString()}/{colorBackfill.total.toLocaleString()} - + + + sampling {colorBackfill.processed.toLocaleString()}/{colorBackfill.total.toLocaleString()} + + ) : } {isActive ? ( + + ) : null}
) : null} diff --git a/src/components/DuplicateFinder.tsx b/src/components/DuplicateFinder.tsx index c0e006d..5d9fc26 100644 --- a/src/components/DuplicateFinder.tsx +++ b/src/components/DuplicateFinder.tsx @@ -3,6 +3,7 @@ import { useVirtualizer } from "@tanstack/react-virtual"; import { DuplicateGroup, useGalleryStore } from "../store"; import { FolderScopeDropdown } from "./FolderScopeDropdown"; import { mediaSrc } from "../lib/mediaSrc"; +import { Tooltip } from "./Tooltip"; function formatBytes(bytes: number): string { if (bytes >= 1_073_741_824) return `${(bytes / 1_073_741_824).toFixed(1)} GB`; @@ -70,6 +71,7 @@ function DuplicateGroupCard({ group }: { group: DuplicateGroup }) { const isSelected = selectedIds.has(image.id); const src = mediaSrc(image.thumbnail_path); return ( + + ); })} @@ -206,13 +208,14 @@ export function DuplicateFinder() { {/* Batch select — only shown when there are groups and nothing is selected yet */} {hasResults && selectedCount === 0 && !deleting && ( + + )} {selectedCount > 0 ? ( <> diff --git a/src/components/ExploreView.tsx b/src/components/ExploreView.tsx index d7c51e3..cb7c7cc 100644 --- a/src/components/ExploreView.tsx +++ b/src/components/ExploreView.tsx @@ -161,6 +161,7 @@ function CloudCard({ node, onOpen, animated }: { node: PlacedNode; onOpen: (imag }; return ( + onOpen(node.entry.image_ids)} - title={`Open cluster — ${node.entry.count.toLocaleString()} ${node.entry.count === 1 ? "image" : "images"}`} > {src ? ( + ); } diff --git a/src/components/FolderPickerModal.tsx b/src/components/FolderPickerModal.tsx index 047b127..a65eda7 100644 --- a/src/components/FolderPickerModal.tsx +++ b/src/components/FolderPickerModal.tsx @@ -1,6 +1,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { useVirtualizer } from "@tanstack/react-virtual"; import { DirEntry, DirListing, FolderAddResult, useGalleryStore } from "../store"; +import { Tooltip } from "./Tooltip"; function normalizePath(path: string): string { return path.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase(); @@ -95,17 +96,18 @@ function FolderRow({ - + + + {alreadyAdded ? ( @@ -113,16 +115,17 @@ function FolderRow({ ) : null} - + + + ); } @@ -164,30 +167,29 @@ function StagedFoldersPanel({ ) : (
{stagedPaths.map((path) => ( -
- - - -
-

{folderName(path)}

-

{path}

-
- -
+
+

{folderName(path)}

+

{path}

+
+ + + +
+ ))} )} @@ -327,16 +329,17 @@ export function FolderPickerModal() {

Add media folders

Choose folders from any location, then add them together.

- + + + @@ -355,14 +358,15 @@ export function FolderPickerModal() { {breadcrumbs.map((crumb, index) => ( {index > 0 ? / : null} - + + + ))} diff --git a/src/components/FolderScopeDropdown.tsx b/src/components/FolderScopeDropdown.tsx index 8657adb..9bbed40 100644 --- a/src/components/FolderScopeDropdown.tsx +++ b/src/components/FolderScopeDropdown.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef, useState } from "react"; import { useGalleryStore } from "../store"; +import { Tooltip } from "./Tooltip"; /** * In-view folder scope picker for feature views (Timeline / Explore / @@ -34,26 +35,27 @@ export function FolderScopeDropdown() { return (
- + + + + {currentLabel} + + + + + {open ? (
+ + + + + ); })} {image.rating > 0 ? ( - + + + ) : null}
@@ -126,7 +127,6 @@ export function ImageTile({ const src = mediaSrc(image.thumbnail_path); return ( -
{image.embedding_status === "failed" && ( -
- - - -
+ +
+ + + +
+
)} {image.favorite && (
@@ -258,8 +257,8 @@ export function ImageTile({
{/* Hover info — appears with overlay */} -
-

{image.filename}

+
+
{image.rating > 0 ? (
@@ -290,6 +289,37 @@ export function ImageTile({
+ ); +} + +function TruncatedFilename({ filename }: { filename: string }) { + const textRef = useRef(null); + const [isTruncated, setIsTruncated] = useState(false); + + useLayoutEffect(() => { + const text = textRef.current; + if (!text) return; + + const update = () => { + setIsTruncated(text.scrollWidth > text.clientWidth); + }; + + update(); + + const observer = new ResizeObserver(update); + observer.observe(text); + return () => observer.disconnect(); + }, [filename]); + + const label = ( +

+ {filename} +

+ ); + + return ( + + {label} ); } diff --git a/src/components/Lightbox.tsx b/src/components/Lightbox.tsx index 6bff094..3665a31 100644 --- a/src/components/Lightbox.tsx +++ b/src/components/Lightbox.tsx @@ -5,6 +5,7 @@ import { revealItemInDir } from "@tauri-apps/plugin-opener"; import { useGalleryStore, ImageTag, ImageExif, AiRating } from "../store"; import { VideoPlayer } from "./VideoPlayer"; import { mediaSrc } from "../lib/mediaSrc"; +import { Tooltip } from "./Tooltip"; function formatBytes(bytes: number): string { if (bytes < 1024) return `${bytes} B`; @@ -525,15 +526,17 @@ export function Lightbox() {

Details

+ + + +
+
)} @@ -618,11 +622,11 @@ export function Lightbox() { {Array.from({ length: 5 }, (_, index) => { const rating = index + 1; return ( + + ); })} {selectedImage.rating > 0 ? ( - + + + ) : null}
@@ -717,10 +723,10 @@ export function Lightbox() { ) : null} {selectedImage.media_kind === "image" ? ( + - ) : null} + ) : null} @@ -736,30 +742,36 @@ export function Lightbox() { <>
{(tagsExpanded ? imageTags : imageTags.slice(0, 8)).map((t) => ( - - {t.tag} - - + {t.tag} + + + + + ))}
{imageTags.length > 8 && ( @@ -914,21 +926,22 @@ export function Lightbox() { ) : null} {imageExif.gps_lat != null && imageExif.gps_lon != null ? ( - + + + ) : null} @@ -937,15 +950,16 @@ export function Lightbox() {

Path

+ +

{selectedImage.path}

diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index 92e43e1..e1ff9d8 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -3,6 +3,7 @@ import { AppTheme, CleanupOrphanedThumbnailsResult, DatabaseInfo, OrphanedThumbn import { FfmpegStatusRow } from "./onboarding/StepWelcome"; import { ThemedDropdown } from "./ThemedDropdown"; import { getChangelogForVersion } from "../changelog"; +import { Tooltip } from "./Tooltip"; type SettingsSection = "workspace" | "general"; @@ -394,15 +395,16 @@ export function SettingsModal() { - + + +
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 90186ef..cfe87cc 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -4,6 +4,7 @@ import { open } from "@tauri-apps/plugin-dialog"; import { useGalleryStore, Folder, Album, IndexProgress } from "../store"; import { ThemedDropdown } from "./ThemedDropdown"; import { mediaSrc } from "../lib/mediaSrc"; +import { Tooltip } from "./Tooltip"; interface ContextMenuState { folderId: number; @@ -181,10 +182,10 @@ function FolderItem({ onContextMenu={handleContextMenu} > {customOrdering ? ( + + ) : null} {isMissing ? ( @@ -273,26 +275,28 @@ function FolderItem({
) : (
- - + + + + + +
) )} @@ -499,10 +503,10 @@ function AlbumItem({ {/* Drag handle — hover-revealed, reorders albums */} {reorderable ? ( + - ) : null} + + ) : null} {/* Cover thumbnail — distinguishes albums from folder rows */}
@@ -833,15 +838,16 @@ export function Sidebar() { {/* Header */}
Phokus + +
{/* Nav */} @@ -985,26 +991,28 @@ export function Sidebar() { ) : (
{albums.length > 0 ? ( + + ) : null} + +
)}
diff --git a/src/components/Toolbar.tsx b/src/components/Toolbar.tsx index 90ef087..846d17c 100644 --- a/src/components/Toolbar.tsx +++ b/src/components/Toolbar.tsx @@ -327,31 +327,33 @@ export function Toolbar() { /> {searchCommand !== null ? (
- + + +
) : null} {searchQuery.trim().length > 0 || searchCommand !== null ? ( - + + + ) : null} diff --git a/src/components/Tooltip.tsx b/src/components/Tooltip.tsx index 6aee866..05a4144 100644 --- a/src/components/Tooltip.tsx +++ b/src/components/Tooltip.tsx @@ -32,6 +32,7 @@ export function Tooltip({ block = false, anchorToCursor = false, followCursor = false, + disabled = false, className = "", children, }: { @@ -47,6 +48,8 @@ export function Tooltip({ anchorToCursor?: boolean; /** Track the cursor (fixed position) instead of anchoring to a side. */ followCursor?: boolean; + /** Render the wrapper but suppress tooltip display. */ + disabled?: boolean; /** Extra classes for the wrapper (e.g. layout). */ className?: string; children: ReactNode; @@ -83,6 +86,7 @@ export function Tooltip({ frame.current = undefined; }; const show = (event: MouseEvent) => { + if (disabled) return; clear(); if (anchorToCursor || followCursor) place(event.clientX, event.clientY); if (delay <= 0) { @@ -110,6 +114,10 @@ export function Tooltip({ return clear; }, []); + useEffect(() => { + if (disabled) hide(); + }, [disabled]); + const Wrapper = block ? "div" : "span"; const cursorTooltip = ( {children} - {anchorToCursor || followCursor ? ( + {disabled ? null : anchorToCursor || followCursor ? ( mounted ? createPortal(cursorTooltip, document.body) : null ) : ( void; - title: string; + label: string; active?: boolean; children: React.ReactNode; }) { return ( - + + + ); } @@ -352,7 +354,7 @@ export function VideoPlayer({ src }: { src: string }) { {/* Control row */}
- + {playing ? ( @@ -371,7 +373,7 @@ export function VideoPlayer({ src }: { src: string }) {
{/* Volume */} - + {effectiveVolume === 0 ? ( @@ -384,30 +386,32 @@ export function VideoPlayer({ src }: { src: string }) { )} - applyVolume(parseFloat(event.target.value), false)} - onClick={(event) => event.stopPropagation()} - title="Volume (↑/↓)" - /> + + applyVolume(parseFloat(event.target.value), false)} + onClick={(event) => event.stopPropagation()} + /> + {/* Playback speed */}
- + + + {speedMenuOpen ? (
{SPEED_OPTIONS.map((rate) => ( @@ -429,14 +433,14 @@ export function VideoPlayer({ src }: { src: string }) {
{/* Loop */} - + {/* Fullscreen */} - + {fullscreen ? ( diff --git a/src/components/WhatsNewModal.tsx b/src/components/WhatsNewModal.tsx index c1015eb..62d9eca 100644 --- a/src/components/WhatsNewModal.tsx +++ b/src/components/WhatsNewModal.tsx @@ -156,15 +156,16 @@ export function WhatsNewModal() { {entry?.date ?

Released {entry.date}

: null}
- + + +
diff --git a/src/components/bulk/BulkTagFields.tsx b/src/components/bulk/BulkTagFields.tsx index 4e7a9dd..b186d5b 100644 --- a/src/components/bulk/BulkTagFields.tsx +++ b/src/components/bulk/BulkTagFields.tsx @@ -1,4 +1,5 @@ import { useBulkTagEditor } from "./useBulkTagEditor"; +import { Tooltip } from "../Tooltip"; // Presentational tag-editing fields shared by the popover and modal surfaces. export function BulkTagFields({ autoFocus = false }: { autoFocus?: boolean }) { @@ -35,14 +36,14 @@ export function BulkTagFields({ autoFocus = false }: { autoFocus?: boolean }) { {suggestions.length > 0 ? (
{suggestions.map((suggestion) => ( - + + + ))}
) : null} @@ -55,15 +56,16 @@ export function BulkTagFields({ autoFocus = false }: { autoFocus?: boolean }) { className="group/chip flex items-center gap-1 rounded-md border border-white/10 bg-white/5 px-2 py-0.5 text-xs text-gray-300" > {tag} - + + + ))}
diff --git a/src/components/bulk/BulkTagPopover.tsx b/src/components/bulk/BulkTagPopover.tsx index fe345b0..4d28e99 100644 --- a/src/components/bulk/BulkTagPopover.tsx +++ b/src/components/bulk/BulkTagPopover.tsx @@ -1,4 +1,5 @@ import { BulkTagFields } from "./BulkTagFields"; +import { Tooltip } from "../Tooltip"; // Inline popover surface for bulk tagging — the default editing surface. // Anchored above the bar by the parent; closes on outside click via the @@ -12,15 +13,16 @@ export function BulkTagPopover({ onClose }: { onClose: () => void }) { >

Add tags

- + + +