diff --git a/src/components/BackgroundTasks.tsx b/src/components/BackgroundTasks.tsx index aa85224..84d78cb 100644 --- a/src/components/BackgroundTasks.tsx +++ b/src/components/BackgroundTasks.tsx @@ -3,6 +3,7 @@ import { invoke } from "@tauri-apps/api/core"; import { revealItemInDir } from "@tauri-apps/plugin-opener"; import { useGalleryStore, WorkerKey } from "../store"; import { Tooltip } from "./Tooltip"; +import { ChevronDownIcon, CloseIcon, PlayIcon, WarningIcon } from "./icons"; const WORKER_FOR_STAGE: Record = { Thumbnails: "thumbnail", @@ -43,10 +44,7 @@ interface FailedWorkerItem { function FailedWorkerItemRow({ item }: { item: FailedWorkerItem }) { return (
- - - +

{item.filename}

{item.error && ( @@ -386,9 +384,7 @@ export function BackgroundTasks() { onClick={(e) => { e.stopPropagation(); toggleWorker(primary.id, workerKey); }} > {isPaused ? ( - - - + ) : ( @@ -448,12 +444,7 @@ export function BackgroundTasks() { {/* Expand chevron (only when multiple tasks) */} {allTasks.length > 1 && ( - - - + )} {/* Dismiss — hidden for system tasks like duplicate scan */} @@ -463,9 +454,7 @@ export function BackgroundTasks() { className="p-1 rounded-md text-gray-600 hover:text-gray-300 hover:bg-white/8 transition-colors shrink-0" onClick={(e) => { e.stopPropagation(); dismissTask(primary.id, primary.snapshot); }} > - - - + )} @@ -518,9 +507,7 @@ export function BackgroundTasks() { onClick={() => toggleWorker(task.id, workerKey)} > {isPaused ? ( - - - + ) : ( @@ -575,9 +562,7 @@ export function BackgroundTasks() { className="p-1 rounded-md text-gray-600 hover:text-gray-300 hover:bg-white/8 transition-colors shrink-0" onClick={() => dismissTask(task.id, task.snapshot)} > - - - + )} diff --git a/src/components/BulkActionBar.tsx b/src/components/BulkActionBar.tsx index d563bbe..e3e480f 100644 --- a/src/components/BulkActionBar.tsx +++ b/src/components/BulkActionBar.tsx @@ -4,6 +4,7 @@ import { AlbumPicker } from "./AlbumPicker"; import { BulkTagPopover } from "./bulk/BulkTagPopover"; import { useDismissable } from "./menu"; import { Tooltip } from "./Tooltip" +import { CloseIcon, StarIcon, WarningIcon } from "./icons"; type Panel = "tag" | "rating" | "album" | "delete" | null; @@ -109,9 +110,7 @@ export function BulkActionBar() { setPanel(null); }} > - - - + ); @@ -174,10 +173,7 @@ export function BulkActionBar() { className="absolute bottom-full left-1/2 mb-2 w-64 -translate-x-1/2 rounded-xl border border-red-500/30 bg-gray-950/98 p-3 shadow-2xl backdrop-blur" >
- - - +

Delete from disk

@@ -207,9 +203,7 @@ export function BulkActionBar() { className="rounded-md p-1.5 text-gray-500 transition-colors hover:bg-white/[0.06] hover:text-white" onClick={clearGallerySelection} > - - - +

diff --git a/src/components/DuplicateFinder.tsx b/src/components/DuplicateFinder.tsx index e228048..a6b546d 100644 --- a/src/components/DuplicateFinder.tsx +++ b/src/components/DuplicateFinder.tsx @@ -4,6 +4,7 @@ import { DuplicateGroup, useGalleryStore } from "../store"; import { FolderScopeDropdown } from "./FolderScopeDropdown"; import { mediaSrc } from "../lib/mediaSrc"; import { Tooltip } from "./Tooltip"; +import { WarningIcon } from "./icons"; function formatBytes(bytes: number): string { if (bytes >= 1_073_741_824) return `${(bytes / 1_073_741_824).toFixed(1)} GB`; @@ -240,10 +241,7 @@ export function DuplicateFinder() {
setConfirmingDelete(false)} />
- - - +

Delete from disk

diff --git a/src/components/FolderPickerModal.tsx b/src/components/FolderPickerModal.tsx index a437c97..467b9fd 100644 --- a/src/components/FolderPickerModal.tsx +++ b/src/components/FolderPickerModal.tsx @@ -2,6 +2,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"; +import { CheckIcon, ChevronRightIcon, CloseIcon } from "./icons"; function normalizePath(path: string): string { return path.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase(); @@ -111,9 +112,7 @@ function FolderRow({ disabled={alreadyAdded} aria-label={selected ? `Remove ${entry.name} from folders to add` : `Choose ${entry.name}`} > - - - + @@ -141,9 +140,7 @@ function FolderRow({ className="rounded-md p-1 text-gray-500 transition-colors hover:bg-white/[0.06] hover:text-white light-theme:hover:bg-gray-900 light-theme:hover:text-white" onClick={onNavigate} > - - - +

@@ -203,9 +200,7 @@ function StagedFoldersPanel({ onClick={() => onRemove(path)} aria-label={`Remove ${path} from folders to add`} > - - - +
@@ -415,9 +410,7 @@ export function FolderPickerModal() { className="rounded-md p-1.5 text-gray-500 transition-colors hover:bg-white/[0.06] hover:text-white light-theme:hover:bg-gray-900 light-theme:hover:text-white" onClick={() => setFolderPickerOpen(false)} > - - - +
diff --git a/src/components/Gallery.tsx b/src/components/Gallery.tsx index fd70c86..2160fae 100644 --- a/src/components/Gallery.tsx +++ b/src/components/Gallery.tsx @@ -5,6 +5,7 @@ import { BulkActionBar } from "./BulkActionBar"; import { ImageContextMenu } from "./ImageContextMenu"; import { Tooltip } from "./Tooltip"; import { mediaSrc } from "../lib/mediaSrc"; +import { CheckIcon, PhotoIcon, PlayIcon, StarIcon, WarningIcon } from "./icons"; const GAP = 6; @@ -85,9 +86,7 @@ export function ImageTile({ : "border-white/70 bg-black/40 text-transparent opacity-0 backdrop-blur-sm group-hover/cb:opacity-100" }`} > - - - + {/* Image / placeholder */} @@ -108,14 +107,9 @@ export function ImageTile({ ) : (
{image.media_kind === "video" ? ( - - - + ) : ( - - - + )}
)} @@ -124,9 +118,7 @@ export function ImageTile({ {image.media_kind === "video" && (
- - - +
)} @@ -136,10 +128,7 @@ export function ImageTile({ {image.embedding_status === "failed" && (
- - - +
)} @@ -153,9 +142,7 @@ export function ImageTile({ {image.rating > 0 && (
{Array.from({ length: image.rating }, (_, index) => ( - - - + ))}
)} @@ -176,9 +163,7 @@ export function ImageTile({ {image.rating > 0 ? (
{Array.from({ length: image.rating }, (_, i) => ( - - - + ))}
) : ( @@ -338,10 +323,7 @@ export function Gallery() { ) : images.length === 0 && !loadingImages ? (
- - - +

{imageLoadError ? "Could not load results" diff --git a/src/components/ImageContextMenu.tsx b/src/components/ImageContextMenu.tsx index 24dc822..086ea79 100644 --- a/src/components/ImageContextMenu.tsx +++ b/src/components/ImageContextMenu.tsx @@ -1,6 +1,7 @@ import { ImageRecord, useGalleryStore } from "../store"; import { ContextMenu, MenuItem, MenuLabel, MenuSeparator, SubMenu } from "./menu"; import { Tooltip } from "./Tooltip"; +import { CloseIcon, StarIcon } from "./icons"; /** Right-click menu for an image tile. Shared by the Gallery grid and the Timeline. */ export function ImageContextMenu({ @@ -58,12 +59,7 @@ export function ImageContextMenu({ className="rounded-md p-1 transition-colors hover:bg-white/5" onClick={async () => { await updateImageDetails(image.id, { rating }); onClose(); }} > - - - + ); @@ -74,9 +70,7 @@ export function ImageContextMenu({ className="ml-1 rounded-md p-1 text-gray-600 hover:bg-white/5 hover:text-gray-300 transition-colors" onClick={async () => { await updateImageDetails(image.id, { rating: 0 }); onClose(); }} > - - - + ) : null} diff --git a/src/components/Lightbox.tsx b/src/components/Lightbox.tsx index 22c4730..977c024 100644 --- a/src/components/Lightbox.tsx +++ b/src/components/Lightbox.tsx @@ -6,6 +6,7 @@ import { useGalleryStore, ImageTag, ImageExif, AiRating } from "../store"; import { VideoPlayer } from "./VideoPlayer"; import { mediaSrc } from "../lib/mediaSrc"; import { Tooltip } from "./Tooltip"; +import { ChevronRightIcon, CloseIcon, StarIcon } from "./icons"; function formatBytes(bytes: number): string { if (bytes < 1024) return `${bytes} B`; @@ -751,9 +752,7 @@ export function Lightbox() { exitSlideshow(); }} > - - - + @@ -811,9 +810,7 @@ export function Lightbox() { goSlideshow(1); }} > - - - +

@@ -1006,9 +1003,7 @@ export function Lightbox() {
@@ -1042,9 +1037,7 @@ export function Lightbox() { ) : regionSelectMode ? ( - - - + Cancel selection ) : ( @@ -1074,13 +1067,7 @@ export function Lightbox() { className="rounded-md p-1" onClick={() => void updateImageDetails(selectedImage.id, { rating })} > - - - + ); @@ -1091,9 +1078,7 @@ export function Lightbox() { className="ml-2 rounded-md border border-white/10 p-1.5 text-gray-400 hover:bg-white/5 hover:text-white" onClick={() => void updateImageDetails(selectedImage.id, { rating: 0 })} > - - - + ) : null} @@ -1211,9 +1196,7 @@ export function Lightbox() { ); }} > - - - + @@ -1426,9 +1409,7 @@ export function Lightbox() { goNext(); }} > - - - + )} diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index cb6ed6c..4a67201 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -5,6 +5,7 @@ import { Dropdown } from "./menu"; import { getChangelogForVersion } from "../changelog"; import { Tooltip } from "./Tooltip"; import { TAGGER_MODELS } from "../taggerModels"; +import { CloseIcon } from "./icons"; type SettingsSection = "general" | "media" | "updates" | "storage" | "workspace"; @@ -440,9 +441,7 @@ export function SettingsModal() { className="rounded-md p-1.5 text-gray-500 transition-colors hover:bg-white/[0.06] hover:text-white" onClick={() => setSettingsOpen(false)} > - - - + diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 83cfe5f..465860a 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -7,6 +7,7 @@ import { InlineConfirm } from "./InlineConfirm"; import { InlineRename } from "./InlineRename"; import { mediaSrc } from "../lib/mediaSrc"; import { Tooltip } from "./Tooltip"; +import { CheckIcon, CloseIcon, FolderIcon, PhotoIcon, PlusIcon } from "./icons"; type LibrarySort = "az" | "za" | "custom"; const LIBRARY_SORT_KEY = "phokus-library-sort"; @@ -146,10 +147,7 @@ function FolderItem({ ) : ( - - - + )}
@@ -204,10 +202,7 @@ function FolderItem({ className="p-1 rounded text-gray-600 hover:text-red-400 hover:bg-red-500/10 transition-colors" onClick={(e) => { e.stopPropagation(); setConfirmingRemoval(true); }} > - - - +
@@ -334,9 +329,7 @@ function AlbumItem({ selectedForManage ? "border-blue-400 bg-blue-500 text-white" : "border-white/30 text-transparent" }`} > - - - + ) : null} @@ -369,10 +362,7 @@ function AlbumItem({ ) : (
- - - +
)} @@ -663,9 +653,7 @@ export function Sidebar() { onClick={handleAddFolder} className="p-1.5 rounded-lg text-gray-500 hover:text-gray-200 hover:bg-white/8 transition-colors" > - - - + @@ -786,9 +774,7 @@ export function Sidebar() { onClick={startCreatingAlbum} className="rounded p-0.5 text-gray-600 transition-colors hover:bg-white/8 hover:text-gray-200" > - - - + diff --git a/src/components/Toolbar.tsx b/src/components/Toolbar.tsx index 76d9c8d..30a0cfe 100644 --- a/src/components/Toolbar.tsx +++ b/src/components/Toolbar.tsx @@ -5,6 +5,7 @@ import { FolderScopeDropdown } from "./FolderScopeDropdown"; import { ColorFilter } from "./ColorFilter"; import { Dropdown, useDismissable } from "./menu"; import { Tooltip } from "./Tooltip"; +import { CloseIcon } from "./icons"; const BASE_SORT_OPTIONS: { value: SortOrder; label: string }[] = [ { value: "date_desc", label: "Newest first" }, @@ -279,9 +280,7 @@ export function Toolbar() { clearSearch(); }} > - - - + diff --git a/src/components/WhatsNewModal.tsx b/src/components/WhatsNewModal.tsx index 62d9eca..eb00071 100644 --- a/src/components/WhatsNewModal.tsx +++ b/src/components/WhatsNewModal.tsx @@ -4,6 +4,7 @@ import { invoke } from "@tauri-apps/api/core"; import { useGalleryStore } from "../store"; import { getChangelogForVersion, type ChangelogSection } from "../changelog"; import { Tooltip } from "./Tooltip"; +import { ChevronRightIcon, CloseIcon } from "./icons"; // Shown in the tooltip so the user can see the link's destination before // clicking. The actual navigation is handled by the open_changelog_url command. @@ -69,14 +70,7 @@ function Section({ section }: { section: ChangelogSection }) { className="flex w-full items-center gap-2 text-left" aria-expanded={open} > - - - + {section.title} {section.items.length} @@ -161,9 +155,7 @@ export function WhatsNewModal() { className="rounded-md p-1.5 text-gray-500 transition-colors hover:bg-white/[0.06] hover:text-white" onClick={closeWhatsNew} > - - - + diff --git a/src/components/bulk/BulkTagFields.tsx b/src/components/bulk/BulkTagFields.tsx index b186d5b..afd66e2 100644 --- a/src/components/bulk/BulkTagFields.tsx +++ b/src/components/bulk/BulkTagFields.tsx @@ -1,5 +1,6 @@ import { useBulkTagEditor } from "./useBulkTagEditor"; import { Tooltip } from "../Tooltip"; +import { CloseIcon } from "../icons"; // Presentational tag-editing fields shared by the popover and modal surfaces. export function BulkTagFields({ autoFocus = false }: { autoFocus?: boolean }) { @@ -61,9 +62,7 @@ export function BulkTagFields({ autoFocus = false }: { autoFocus?: boolean }) { className="text-gray-600 transition-colors hover:text-white" onClick={() => void removeTag(tag)} > - - - + diff --git a/src/components/bulk/BulkTagPopover.tsx b/src/components/bulk/BulkTagPopover.tsx index 4d28e99..5d92da1 100644 --- a/src/components/bulk/BulkTagPopover.tsx +++ b/src/components/bulk/BulkTagPopover.tsx @@ -1,5 +1,6 @@ import { BulkTagFields } from "./BulkTagFields"; import { Tooltip } from "../Tooltip"; +import { CloseIcon } from "../icons"; // Inline popover surface for bulk tagging — the default editing surface. // Anchored above the bar by the parent; closes on outside click via the @@ -18,9 +19,7 @@ export function BulkTagPopover({ onClose }: { onClose: () => void }) { className="text-gray-600 transition-colors hover:text-white" onClick={onClose} > - - - + diff --git a/src/components/icons.tsx b/src/components/icons.tsx new file mode 100644 index 0000000..b57b549 --- /dev/null +++ b/src/components/icons.tsx @@ -0,0 +1,60 @@ +/** + * Shared icons for paths that repeat across the app. One-off icons stay + * inline at their call site — only extract here once a shape shows up in + * three or more places. Stroke icons take a per-site strokeWidth because + * weights legitimately differ by context (menus vs badges vs empty states). + */ + +export interface IconProps { + className?: string; + strokeWidth?: number; +} + +function strokeIcon(d: string, defaultStrokeWidth: number, displayName: string) { + function Icon({ className = "", strokeWidth = defaultStrokeWidth }: IconProps) { + return ( + + ); + } + Icon.displayName = displayName; + return Icon; +} + +export const CheckIcon = strokeIcon("M5 13l4 4L19 7", 2.5, "CheckIcon"); +export const CloseIcon = strokeIcon("M6 18L18 6M6 6l12 12", 2, "CloseIcon"); +export const ChevronDownIcon = strokeIcon("M19 9l-7 7-7-7", 2, "ChevronDownIcon"); +export const ChevronRightIcon = strokeIcon("M9 5l7 7-7 7", 2, "ChevronRightIcon"); +export const PlusIcon = strokeIcon("M12 4v16m8-8H4", 1.75, "PlusIcon"); +export const PhotoIcon = strokeIcon( + "M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z", + 1.5, + "PhotoIcon", +); +export const FolderIcon = strokeIcon( + "M3 7a2 2 0 012-2h3.586a1 1 0 01.707.293l1.414 1.414A1 1 0 0011.414 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V7z", + 1.5, + "FolderIcon", +); +export const WarningIcon = strokeIcon( + "M12 9v2m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z", + 2, + "WarningIcon", +); + +export function StarIcon({ className = "" }: { className?: string }) { + return ( + + ); +} + +export function PlayIcon({ className = "" }: { className?: string }) { + return ( + + ); +} diff --git a/src/components/menu/Dropdown.tsx b/src/components/menu/Dropdown.tsx index 1832108..c889076 100644 --- a/src/components/menu/Dropdown.tsx +++ b/src/components/menu/Dropdown.tsx @@ -2,6 +2,7 @@ import { ReactNode, useRef, useState } from "react"; import { MenuCloseContext, MenuItem, MenuPanel, MenuSize } from "./Menu"; import { useDismissable } from "./useDismissable"; import { Tooltip } from "../Tooltip"; +import { ChevronDownIcon } from "../icons"; export interface DropdownOption { value: T; @@ -85,14 +86,7 @@ export function Dropdown({ > {triggerIcon} {current?.label} - - - + ); diff --git a/src/components/menu/Menu.tsx b/src/components/menu/Menu.tsx index 049d2d7..4143a4b 100644 --- a/src/components/menu/Menu.tsx +++ b/src/components/menu/Menu.tsx @@ -7,6 +7,7 @@ import { useRef, useState, } from "react"; +import { CheckIcon, ChevronRightIcon } from "../icons"; export type MenuSize = "sm" | "md"; @@ -115,9 +116,7 @@ export function MenuItem({ {hint} ) : null} {checked ? ( - - - + ) : null} ); @@ -206,9 +205,7 @@ export function SubMenu({ onClick={openNow} > {label} - - - + {open ? (