refactor(ui): extract shared icons into icons.tsx

The ten most-duplicated inline SVGs (Check, Close, ChevronDown/Right,
Plus, Photo, Folder, Warning, Star, Play) become shared components --
34+ copies across 15 files replaced. Stroke icons take a per-site
strokeWidth since weights legitimately differ by context; icons that
appear once or use variant path data stay inline.
This commit is contained in:
2026-07-04 12:29:14 +01:00
parent 4d41f3744f
commit fbf8a7878c
16 changed files with 122 additions and 170 deletions
+7 -22
View File
@@ -3,6 +3,7 @@ import { invoke } from "@tauri-apps/api/core";
import { revealItemInDir } from "@tauri-apps/plugin-opener"; import { revealItemInDir } from "@tauri-apps/plugin-opener";
import { useGalleryStore, WorkerKey } from "../store"; import { useGalleryStore, WorkerKey } from "../store";
import { Tooltip } from "./Tooltip"; import { Tooltip } from "./Tooltip";
import { ChevronDownIcon, CloseIcon, PlayIcon, WarningIcon } from "./icons";
const WORKER_FOR_STAGE: Record<string, WorkerKey> = { const WORKER_FOR_STAGE: Record<string, WorkerKey> = {
Thumbnails: "thumbnail", Thumbnails: "thumbnail",
@@ -43,10 +44,7 @@ interface FailedWorkerItem {
function FailedWorkerItemRow({ item }: { item: FailedWorkerItem }) { function FailedWorkerItemRow({ item }: { item: FailedWorkerItem }) {
return ( return (
<div className="flex min-w-0 items-start gap-1.5"> <div className="flex min-w-0 items-start gap-1.5">
<svg className="mt-px h-2.5 w-2.5 shrink-0 text-amber-500 light-theme:text-amber-700" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <WarningIcon className="mt-px h-2.5 w-2.5 shrink-0 text-amber-500 light-theme:text-amber-700" strokeWidth={2.5} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5}
d="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" />
</svg>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="truncate text-[10px] font-medium text-amber-400/80 light-theme:text-amber-700">{item.filename}</p> <p className="truncate text-[10px] font-medium text-amber-400/80 light-theme:text-amber-700">{item.filename}</p>
{item.error && ( {item.error && (
@@ -386,9 +384,7 @@ export function BackgroundTasks() {
onClick={(e) => { e.stopPropagation(); toggleWorker(primary.id, workerKey); }} onClick={(e) => { e.stopPropagation(); toggleWorker(primary.id, workerKey); }}
> >
{isPaused ? ( {isPaused ? (
<svg className="h-2.5 w-2.5" fill="currentColor" viewBox="0 0 24 24"> <PlayIcon className="h-2.5 w-2.5" />
<path d="M8 5v14l11-7z" />
</svg>
) : ( ) : (
<svg className="h-2.5 w-2.5" fill="currentColor" viewBox="0 0 24 24"> <svg className="h-2.5 w-2.5" fill="currentColor" viewBox="0 0 24 24">
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" /> <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
@@ -448,12 +444,7 @@ export function BackgroundTasks() {
{/* Expand chevron (only when multiple tasks) */} {/* Expand chevron (only when multiple tasks) */}
{allTasks.length > 1 && ( {allTasks.length > 1 && (
<svg <ChevronDownIcon className={`h-3.5 w-3.5 text-gray-600 transition-transform duration-200 shrink-0 ${expanded ? "rotate-180" : ""}`} />
className={`h-3.5 w-3.5 text-gray-600 transition-transform duration-200 shrink-0 ${expanded ? "rotate-180" : ""}`}
fill="none" viewBox="0 0 24 24" stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
)} )}
{/* Dismiss — hidden for system tasks like duplicate scan */} {/* 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" 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); }} onClick={(e) => { e.stopPropagation(); dismissTask(primary.id, primary.snapshot); }}
> >
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <CloseIcon className="h-3.5 w-3.5" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button> </button>
</Tooltip> </Tooltip>
)} )}
@@ -518,9 +507,7 @@ export function BackgroundTasks() {
onClick={() => toggleWorker(task.id, workerKey)} onClick={() => toggleWorker(task.id, workerKey)}
> >
{isPaused ? ( {isPaused ? (
<svg className="h-2.5 w-2.5" fill="currentColor" viewBox="0 0 24 24"> <PlayIcon className="h-2.5 w-2.5" />
<path d="M8 5v14l11-7z" />
</svg>
) : ( ) : (
<svg className="h-2.5 w-2.5" fill="currentColor" viewBox="0 0 24 24"> <svg className="h-2.5 w-2.5" fill="currentColor" viewBox="0 0 24 24">
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" /> <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
@@ -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" 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)} onClick={() => dismissTask(task.id, task.snapshot)}
> >
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <CloseIcon className="h-3 w-3" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button> </button>
</Tooltip> </Tooltip>
)} )}
+4 -10
View File
@@ -4,6 +4,7 @@ import { AlbumPicker } from "./AlbumPicker";
import { BulkTagPopover } from "./bulk/BulkTagPopover"; import { BulkTagPopover } from "./bulk/BulkTagPopover";
import { useDismissable } from "./menu"; import { useDismissable } from "./menu";
import { Tooltip } from "./Tooltip" import { Tooltip } from "./Tooltip"
import { CloseIcon, StarIcon, WarningIcon } from "./icons";
type Panel = "tag" | "rating" | "album" | "delete" | null; type Panel = "tag" | "rating" | "album" | "delete" | null;
@@ -109,9 +110,7 @@ export function BulkActionBar() {
setPanel(null); setPanel(null);
}} }}
> >
<svg className="h-4 w-4" fill="currentColor" viewBox="0 0 20 20"> <StarIcon className="h-4 w-4" />
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.176 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81H7.03a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
</button> </button>
</Tooltip> </Tooltip>
); );
@@ -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" 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"
> >
<div className="mb-1 flex items-center gap-1.5 text-red-300"> <div className="mb-1 flex items-center gap-1.5 text-red-300">
<svg className="h-3.5 w-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <WarningIcon className="h-3.5 w-3.5 shrink-0" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="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" />
</svg>
<p className="text-xs font-semibold">Delete from disk</p> <p className="text-xs font-semibold">Delete from disk</p>
</div> </div>
<p className="mb-2.5 text-[11px] leading-relaxed text-gray-400"> <p className="mb-2.5 text-[11px] leading-relaxed text-gray-400">
@@ -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" className="rounded-md p-1.5 text-gray-500 transition-colors hover:bg-white/[0.06] hover:text-white"
onClick={clearGallerySelection} onClick={clearGallerySelection}
> >
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <CloseIcon className="h-4 w-4" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button> </button>
</Tooltip> </Tooltip>
</div> </div>
+2 -4
View File
@@ -4,6 +4,7 @@ import { DuplicateGroup, useGalleryStore } from "../store";
import { FolderScopeDropdown } from "./FolderScopeDropdown"; import { FolderScopeDropdown } from "./FolderScopeDropdown";
import { mediaSrc } from "../lib/mediaSrc"; import { mediaSrc } from "../lib/mediaSrc";
import { Tooltip } from "./Tooltip"; import { Tooltip } from "./Tooltip";
import { WarningIcon } from "./icons";
function formatBytes(bytes: number): string { function formatBytes(bytes: number): string {
if (bytes >= 1_073_741_824) return `${(bytes / 1_073_741_824).toFixed(1)} GB`; if (bytes >= 1_073_741_824) return `${(bytes / 1_073_741_824).toFixed(1)} GB`;
@@ -240,10 +241,7 @@ export function DuplicateFinder() {
<div className="fixed inset-0 z-40" onClick={() => setConfirmingDelete(false)} /> <div className="fixed inset-0 z-40" onClick={() => setConfirmingDelete(false)} />
<div className="absolute right-0 top-full z-50 mt-2 w-72 rounded-xl border border-red-500/30 bg-gray-950/98 p-3 text-left shadow-2xl backdrop-blur"> <div className="absolute right-0 top-full z-50 mt-2 w-72 rounded-xl border border-red-500/30 bg-gray-950/98 p-3 text-left shadow-2xl backdrop-blur">
<div className="mb-1 flex items-center gap-1.5 text-red-300"> <div className="mb-1 flex items-center gap-1.5 text-red-300">
<svg className="h-3.5 w-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <WarningIcon className="h-3.5 w-3.5 shrink-0" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="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" />
</svg>
<p className="text-xs font-semibold">Delete from disk</p> <p className="text-xs font-semibold">Delete from disk</p>
</div> </div>
<p className="mb-2.5 text-[11px] leading-relaxed text-gray-400"> <p className="mb-2.5 text-[11px] leading-relaxed text-gray-400">
+5 -12
View File
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { useVirtualizer } from "@tanstack/react-virtual"; import { useVirtualizer } from "@tanstack/react-virtual";
import { DirEntry, DirListing, FolderAddResult, useGalleryStore } from "../store"; import { DirEntry, DirListing, FolderAddResult, useGalleryStore } from "../store";
import { Tooltip } from "./Tooltip"; import { Tooltip } from "./Tooltip";
import { CheckIcon, ChevronRightIcon, CloseIcon } from "./icons";
function normalizePath(path: string): string { function normalizePath(path: string): string {
return path.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase(); return path.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
@@ -111,9 +112,7 @@ function FolderRow({
disabled={alreadyAdded} disabled={alreadyAdded}
aria-label={selected ? `Remove ${entry.name} from folders to add` : `Choose ${entry.name}`} aria-label={selected ? `Remove ${entry.name} from folders to add` : `Choose ${entry.name}`}
> >
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <CheckIcon className="h-3.5 w-3.5" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
</svg>
</button> </button>
<Tooltip label={entry.path} anchorToCursor className="min-w-0 flex-1"> <Tooltip label={entry.path} anchorToCursor className="min-w-0 flex-1">
@@ -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" 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} onClick={onNavigate}
> >
<svg className={`h-4 w-4 ${entry.has_children ? "" : "opacity-45"}`} fill="none" viewBox="0 0 24 24" stroke="currentColor"> <ChevronRightIcon className={`h-4 w-4 ${entry.has_children ? "" : "opacity-45"}`} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button> </button>
</Tooltip> </Tooltip>
</div> </div>
@@ -203,9 +200,7 @@ function StagedFoldersPanel({
onClick={() => onRemove(path)} onClick={() => onRemove(path)}
aria-label={`Remove ${path} from folders to add`} aria-label={`Remove ${path} from folders to add`}
> >
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <CloseIcon className="h-3.5 w-3.5" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button> </button>
</Tooltip> </Tooltip>
</div> </div>
@@ -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" 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)} onClick={() => setFolderPickerOpen(false)}
> >
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <CloseIcon className="h-4 w-4" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button> </button>
</Tooltip> </Tooltip>
</div> </div>
+9 -27
View File
@@ -5,6 +5,7 @@ import { BulkActionBar } from "./BulkActionBar";
import { ImageContextMenu } from "./ImageContextMenu"; import { ImageContextMenu } from "./ImageContextMenu";
import { Tooltip } from "./Tooltip"; import { Tooltip } from "./Tooltip";
import { mediaSrc } from "../lib/mediaSrc"; import { mediaSrc } from "../lib/mediaSrc";
import { CheckIcon, PhotoIcon, PlayIcon, StarIcon, WarningIcon } from "./icons";
const GAP = 6; 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" : "border-white/70 bg-black/40 text-transparent opacity-0 backdrop-blur-sm group-hover/cb:opacity-100"
}`} }`}
> >
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <CheckIcon className="h-3 w-3" strokeWidth={3} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
</div> </div>
</button> </button>
{/* Image / placeholder */} {/* Image / placeholder */}
@@ -108,14 +107,9 @@ export function ImageTile({
) : ( ) : (
<div className="absolute inset-0 flex items-center justify-center bg-white/[0.03] text-white/20"> <div className="absolute inset-0 flex items-center justify-center bg-white/[0.03] text-white/20">
{image.media_kind === "video" ? ( {image.media_kind === "video" ? (
<svg className="h-7 w-7" fill="currentColor" viewBox="0 0 24 24"> <PlayIcon className="h-7 w-7" />
<path d="M8 5v14l11-7z" />
</svg>
) : ( ) : (
<svg className="h-7 w-7" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <PhotoIcon className="h-7 w-7" strokeWidth={1} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1}
d="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" />
</svg>
)} )}
</div> </div>
)} )}
@@ -124,9 +118,7 @@ export function ImageTile({
{image.media_kind === "video" && ( {image.media_kind === "video" && (
<div className="absolute inset-0 flex items-center justify-center pointer-events-none"> <div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<div className="rounded-full bg-black/40 p-3 text-white backdrop-blur-sm opacity-50 group-hover:opacity-90 transition-opacity duration-200"> <div className="rounded-full bg-black/40 p-3 text-white backdrop-blur-sm opacity-50 group-hover:opacity-90 transition-opacity duration-200">
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"> <PlayIcon className="h-5 w-5" />
<path d="M8 5v14l11-7z" />
</svg>
</div> </div>
</div> </div>
)} )}
@@ -136,10 +128,7 @@ export function ImageTile({
{image.embedding_status === "failed" && ( {image.embedding_status === "failed" && (
<Tooltip label={image.embedding_error ?? "Embedding failed"} followCursor className="pointer-events-auto"> <Tooltip label={image.embedding_error ?? "Embedding failed"} followCursor className="pointer-events-auto">
<div className="flex items-center gap-1 rounded-md bg-black/60 px-1.5 py-0.5 text-[10px] font-medium text-amber-400 backdrop-blur-sm"> <div className="flex items-center gap-1 rounded-md bg-black/60 px-1.5 py-0.5 text-[10px] font-medium text-amber-400 backdrop-blur-sm">
<svg className="h-2.5 w-2.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <WarningIcon className="h-2.5 w-2.5 shrink-0" strokeWidth={2.5} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5}
d="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" />
</svg>
</div> </div>
</Tooltip> </Tooltip>
)} )}
@@ -153,9 +142,7 @@ export function ImageTile({
{image.rating > 0 && ( {image.rating > 0 && (
<div className="flex items-center gap-0.5 rounded-md bg-black/60 px-1.5 py-1 text-amber-300 backdrop-blur-sm"> <div className="flex items-center gap-0.5 rounded-md bg-black/60 px-1.5 py-1 text-amber-300 backdrop-blur-sm">
{Array.from({ length: image.rating }, (_, index) => ( {Array.from({ length: image.rating }, (_, index) => (
<svg key={index} className="h-2.5 w-2.5" fill="currentColor" viewBox="0 0 20 20"> <StarIcon key={index} className="h-2.5 w-2.5" />
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.176 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81H7.03a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
))} ))}
</div> </div>
)} )}
@@ -176,9 +163,7 @@ export function ImageTile({
{image.rating > 0 ? ( {image.rating > 0 ? (
<div className="flex items-center gap-0.5"> <div className="flex items-center gap-0.5">
{Array.from({ length: image.rating }, (_, i) => ( {Array.from({ length: image.rating }, (_, i) => (
<svg key={i} className="h-2.5 w-2.5 text-amber-300" fill="currentColor" viewBox="0 0 20 20"> <StarIcon key={i} className="h-2.5 w-2.5 text-amber-300" />
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.176 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81H7.03a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
))} ))}
</div> </div>
) : ( ) : (
@@ -338,10 +323,7 @@ export function Gallery() {
) : images.length === 0 && !loadingImages ? ( ) : images.length === 0 && !loadingImages ? (
<div className="flex flex-1 flex-col items-center justify-center gap-4 text-center px-8 absolute inset-0"> <div className="flex flex-1 flex-col items-center justify-center gap-4 text-center px-8 absolute inset-0">
<div className="rounded-2xl border border-white/[0.06] bg-white/[0.02] p-8"> <div className="rounded-2xl border border-white/[0.06] bg-white/[0.02] p-8">
<svg className="h-12 w-12 mx-auto text-white/10 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <PhotoIcon className="h-12 w-12 mx-auto text-white/10 mb-4" strokeWidth={0.75} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={0.75}
d="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" />
</svg>
<p className="text-sm text-white/30 font-medium"> <p className="text-sm text-white/30 font-medium">
{imageLoadError {imageLoadError
? "Could not load results" ? "Could not load results"
+3 -9
View File
@@ -1,6 +1,7 @@
import { ImageRecord, useGalleryStore } from "../store"; import { ImageRecord, useGalleryStore } from "../store";
import { ContextMenu, MenuItem, MenuLabel, MenuSeparator, SubMenu } from "./menu"; import { ContextMenu, MenuItem, MenuLabel, MenuSeparator, SubMenu } from "./menu";
import { Tooltip } from "./Tooltip"; import { Tooltip } from "./Tooltip";
import { CloseIcon, StarIcon } from "./icons";
/** Right-click menu for an image tile. Shared by the Gallery grid and the Timeline. */ /** Right-click menu for an image tile. Shared by the Gallery grid and the Timeline. */
export function ImageContextMenu({ export function ImageContextMenu({
@@ -58,12 +59,7 @@ export function ImageContextMenu({
className="rounded-md p-1 transition-colors hover:bg-white/5" className="rounded-md p-1 transition-colors hover:bg-white/5"
onClick={async () => { await updateImageDetails(image.id, { rating }); onClose(); }} onClick={async () => { await updateImageDetails(image.id, { rating }); onClose(); }}
> >
<svg <StarIcon className={`h-4 w-4 ${rating <= image.rating ? "text-amber-300" : "text-white/20 hover:text-white/40"}`} />
className={`h-4 w-4 ${rating <= image.rating ? "text-amber-300" : "text-white/20 hover:text-white/40"}`}
fill="currentColor" viewBox="0 0 20 20"
>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.176 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81H7.03a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
</button> </button>
</Tooltip> </Tooltip>
); );
@@ -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" 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(); }} onClick={async () => { await updateImageDetails(image.id, { rating: 0 }); onClose(); }}
> >
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <CloseIcon className="h-3 w-3" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button> </button>
</Tooltip> </Tooltip>
) : null} ) : null}
+9 -28
View File
@@ -6,6 +6,7 @@ import { useGalleryStore, ImageTag, ImageExif, AiRating } from "../store";
import { VideoPlayer } from "./VideoPlayer"; import { VideoPlayer } from "./VideoPlayer";
import { mediaSrc } from "../lib/mediaSrc"; import { mediaSrc } from "../lib/mediaSrc";
import { Tooltip } from "./Tooltip"; import { Tooltip } from "./Tooltip";
import { ChevronRightIcon, CloseIcon, StarIcon } from "./icons";
function formatBytes(bytes: number): string { function formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`; if (bytes < 1024) return `${bytes} B`;
@@ -751,9 +752,7 @@ export function Lightbox() {
exitSlideshow(); exitSlideshow();
}} }}
> >
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <CloseIcon className="h-4 w-4" strokeWidth={1.8} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M6 18L18 6M6 6l12 12" />
</svg>
</button> </button>
</Tooltip> </Tooltip>
</motion.div> </motion.div>
@@ -811,9 +810,7 @@ export function Lightbox() {
goSlideshow(1); goSlideshow(1);
}} }}
> >
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <ChevronRightIcon className="h-5 w-5" strokeWidth={1.8} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M9 5l7 7-7 7" />
</svg>
</button> </button>
</Tooltip> </Tooltip>
</div> </div>
@@ -1006,9 +1003,7 @@ export function Lightbox() {
</Tooltip> </Tooltip>
</div> </div>
<button className="rounded p-1 text-gray-400 hover:text-white" onClick={closeImage}> <button className="rounded p-1 text-gray-400 hover:text-white" onClick={closeImage}>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <CloseIcon className="h-4 w-4" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button> </button>
</div> </div>
@@ -1042,9 +1037,7 @@ export function Lightbox() {
</span> </span>
) : regionSelectMode ? ( ) : regionSelectMode ? (
<span className="flex items-center justify-center gap-1.5"> <span className="flex items-center justify-center gap-1.5">
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <CloseIcon className="h-3 w-3" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
Cancel selection Cancel selection
</span> </span>
) : ( ) : (
@@ -1074,13 +1067,7 @@ export function Lightbox() {
className="rounded-md p-1" className="rounded-md p-1"
onClick={() => void updateImageDetails(selectedImage.id, { rating })} onClick={() => void updateImageDetails(selectedImage.id, { rating })}
> >
<svg <StarIcon className={`h-5 w-5 ${rating <= selectedImage.rating ? "text-amber-300" : "text-white/20 hover:text-white/50"}`} />
className={`h-5 w-5 ${rating <= selectedImage.rating ? "text-amber-300" : "text-white/20 hover:text-white/50"}`}
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.176 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81H7.03a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
</button> </button>
</Tooltip> </Tooltip>
); );
@@ -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" 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 })} onClick={() => void updateImageDetails(selectedImage.id, { rating: 0 })}
> >
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <CloseIcon className="h-3.5 w-3.5" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button> </button>
</Tooltip> </Tooltip>
) : null} ) : null}
@@ -1211,9 +1196,7 @@ export function Lightbox() {
); );
}} }}
> >
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <CloseIcon className="h-3 w-3" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button> </button>
</Tooltip> </Tooltip>
</span> </span>
@@ -1426,9 +1409,7 @@ export function Lightbox() {
goNext(); goNext();
}} }}
> >
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <ChevronRightIcon className="h-5 w-5" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button> </button>
</> </>
)} )}
+2 -3
View File
@@ -5,6 +5,7 @@ import { Dropdown } from "./menu";
import { getChangelogForVersion } from "../changelog"; import { getChangelogForVersion } from "../changelog";
import { Tooltip } from "./Tooltip"; import { Tooltip } from "./Tooltip";
import { TAGGER_MODELS } from "../taggerModels"; import { TAGGER_MODELS } from "../taggerModels";
import { CloseIcon } from "./icons";
type SettingsSection = "general" | "media" | "updates" | "storage" | "workspace"; 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" className="rounded-md p-1.5 text-gray-500 transition-colors hover:bg-white/[0.06] hover:text-white"
onClick={() => setSettingsOpen(false)} onClick={() => setSettingsOpen(false)}
> >
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <CloseIcon className="h-4 w-4" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button> </button>
</Tooltip> </Tooltip>
</div> </div>
+7 -21
View File
@@ -7,6 +7,7 @@ import { InlineConfirm } from "./InlineConfirm";
import { InlineRename } from "./InlineRename"; import { InlineRename } from "./InlineRename";
import { mediaSrc } from "../lib/mediaSrc"; import { mediaSrc } from "../lib/mediaSrc";
import { Tooltip } from "./Tooltip"; import { Tooltip } from "./Tooltip";
import { CheckIcon, CloseIcon, FolderIcon, PhotoIcon, PlusIcon } from "./icons";
type LibrarySort = "az" | "za" | "custom"; type LibrarySort = "az" | "za" | "custom";
const LIBRARY_SORT_KEY = "phokus-library-sort"; const LIBRARY_SORT_KEY = "phokus-library-sort";
@@ -146,10 +147,7 @@ function FolderItem({
</svg> </svg>
</span> </span>
) : ( ) : (
<svg className="w-3.5 h-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <FolderIcon className="w-3.5 h-3.5 shrink-0" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
d="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" />
</svg>
)} )}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
@@ -204,10 +202,7 @@ function FolderItem({
className="p-1 rounded text-gray-600 hover:text-red-400 hover:bg-red-500/10 transition-colors" className="p-1 rounded text-gray-600 hover:text-red-400 hover:bg-red-500/10 transition-colors"
onClick={(e) => { e.stopPropagation(); setConfirmingRemoval(true); }} onClick={(e) => { e.stopPropagation(); setConfirmingRemoval(true); }}
> >
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <CloseIcon className="w-3 h-3" strokeWidth={1.75} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.75}
d="M6 18L18 6M6 6l12 12" />
</svg>
</button> </button>
</Tooltip> </Tooltip>
</div> </div>
@@ -334,9 +329,7 @@ function AlbumItem({
selectedForManage ? "border-blue-400 bg-blue-500 text-white" : "border-white/30 text-transparent" selectedForManage ? "border-blue-400 bg-blue-500 text-white" : "border-white/30 text-transparent"
}`} }`}
> >
<svg className="h-2.5 w-2.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <CheckIcon className="h-2.5 w-2.5" strokeWidth={3} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
</div> </div>
) : null} ) : null}
@@ -369,10 +362,7 @@ function AlbumItem({
<img src={cover} alt="" className="h-full w-full object-cover" /> <img src={cover} alt="" className="h-full w-full object-cover" />
) : ( ) : (
<div className="flex h-full w-full items-center justify-center text-white/20"> <div className="flex h-full w-full items-center justify-center text-white/20">
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <PhotoIcon className="h-3.5 w-3.5" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
d="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" />
</svg>
</div> </div>
)} )}
</div> </div>
@@ -663,9 +653,7 @@ export function Sidebar() {
onClick={handleAddFolder} onClick={handleAddFolder}
className="p-1.5 rounded-lg text-gray-500 hover:text-gray-200 hover:bg-white/8 transition-colors" className="p-1.5 rounded-lg text-gray-500 hover:text-gray-200 hover:bg-white/8 transition-colors"
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <PlusIcon className="w-4 h-4" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.75} d="M12 4v16m8-8H4" />
</svg>
</button> </button>
</Tooltip> </Tooltip>
</div> </div>
@@ -786,9 +774,7 @@ export function Sidebar() {
onClick={startCreatingAlbum} onClick={startCreatingAlbum}
className="rounded p-0.5 text-gray-600 transition-colors hover:bg-white/8 hover:text-gray-200" className="rounded p-0.5 text-gray-600 transition-colors hover:bg-white/8 hover:text-gray-200"
> >
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <PlusIcon className="h-3.5 w-3.5" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.75} d="M12 4v16m8-8H4" />
</svg>
</button> </button>
</Tooltip> </Tooltip>
</div> </div>
+2 -3
View File
@@ -5,6 +5,7 @@ import { FolderScopeDropdown } from "./FolderScopeDropdown";
import { ColorFilter } from "./ColorFilter"; import { ColorFilter } from "./ColorFilter";
import { Dropdown, useDismissable } from "./menu"; import { Dropdown, useDismissable } from "./menu";
import { Tooltip } from "./Tooltip"; import { Tooltip } from "./Tooltip";
import { CloseIcon } from "./icons";
const BASE_SORT_OPTIONS: { value: SortOrder; label: string }[] = [ const BASE_SORT_OPTIONS: { value: SortOrder; label: string }[] = [
{ value: "date_desc", label: "Newest first" }, { value: "date_desc", label: "Newest first" },
@@ -279,9 +280,7 @@ export function Toolbar() {
clearSearch(); clearSearch();
}} }}
> >
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <CloseIcon className="h-3.5 w-3.5" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button> </button>
</Tooltip> </Tooltip>
</div> </div>
+3 -11
View File
@@ -4,6 +4,7 @@ import { invoke } from "@tauri-apps/api/core";
import { useGalleryStore } from "../store"; import { useGalleryStore } from "../store";
import { getChangelogForVersion, type ChangelogSection } from "../changelog"; import { getChangelogForVersion, type ChangelogSection } from "../changelog";
import { Tooltip } from "./Tooltip"; import { Tooltip } from "./Tooltip";
import { ChevronRightIcon, CloseIcon } from "./icons";
// Shown in the tooltip so the user can see the link's destination before // 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. // 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" className="flex w-full items-center gap-2 text-left"
aria-expanded={open} aria-expanded={open}
> >
<svg <ChevronRightIcon className={`h-3 w-3 shrink-0 text-gray-600 transition-transform ${open ? "rotate-90" : ""}`} strokeWidth={2.5} />
className={`h-3 w-3 shrink-0 text-gray-600 transition-transform ${open ? "rotate-90" : ""}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M9 5l7 7-7 7" />
</svg>
<span className={`text-[11px] font-semibold uppercase tracking-[0.1em] ${style.label}`}>{section.title}</span> <span className={`text-[11px] font-semibold uppercase tracking-[0.1em] ${style.label}`}>{section.title}</span>
<span className="text-[11px] font-medium text-gray-600">{section.items.length}</span> <span className="text-[11px] font-medium text-gray-600">{section.items.length}</span>
</button> </button>
@@ -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" className="rounded-md p-1.5 text-gray-500 transition-colors hover:bg-white/[0.06] hover:text-white"
onClick={closeWhatsNew} onClick={closeWhatsNew}
> >
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <CloseIcon className="h-4 w-4" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button> </button>
</Tooltip> </Tooltip>
</div> </div>
+2 -3
View File
@@ -1,5 +1,6 @@
import { useBulkTagEditor } from "./useBulkTagEditor"; import { useBulkTagEditor } from "./useBulkTagEditor";
import { Tooltip } from "../Tooltip"; import { Tooltip } from "../Tooltip";
import { CloseIcon } from "../icons";
// Presentational tag-editing fields shared by the popover and modal surfaces. // Presentational tag-editing fields shared by the popover and modal surfaces.
export function BulkTagFields({ autoFocus = false }: { autoFocus?: boolean }) { 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" className="text-gray-600 transition-colors hover:text-white"
onClick={() => void removeTag(tag)} onClick={() => void removeTag(tag)}
> >
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <CloseIcon className="h-3 w-3" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button> </button>
</Tooltip> </Tooltip>
</span> </span>
+2 -3
View File
@@ -1,5 +1,6 @@
import { BulkTagFields } from "./BulkTagFields"; import { BulkTagFields } from "./BulkTagFields";
import { Tooltip } from "../Tooltip"; import { Tooltip } from "../Tooltip";
import { CloseIcon } from "../icons";
// Inline popover surface for bulk tagging — the default editing surface. // Inline popover surface for bulk tagging — the default editing surface.
// Anchored above the bar by the parent; closes on outside click via the // 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" className="text-gray-600 transition-colors hover:text-white"
onClick={onClose} onClick={onClose}
> >
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <CloseIcon className="h-3.5 w-3.5" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button> </button>
</Tooltip> </Tooltip>
</div> </div>
+60
View File
@@ -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 (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={strokeWidth} d={d} />
</svg>
);
}
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 (
<svg className={className} fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.176 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81H7.03a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
);
}
export function PlayIcon({ className = "" }: { className?: string }) {
return (
<svg className={className} fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M8 5v14l11-7z" />
</svg>
);
}
+2 -8
View File
@@ -2,6 +2,7 @@ import { ReactNode, useRef, useState } from "react";
import { MenuCloseContext, MenuItem, MenuPanel, MenuSize } from "./Menu"; import { MenuCloseContext, MenuItem, MenuPanel, MenuSize } from "./Menu";
import { useDismissable } from "./useDismissable"; import { useDismissable } from "./useDismissable";
import { Tooltip } from "../Tooltip"; import { Tooltip } from "../Tooltip";
import { ChevronDownIcon } from "../icons";
export interface DropdownOption<T> { export interface DropdownOption<T> {
value: T; value: T;
@@ -85,14 +86,7 @@ export function Dropdown<T extends string | number | null>({
> >
{triggerIcon} {triggerIcon}
<span className="min-w-0 truncate">{current?.label}</span> <span className="min-w-0 truncate">{current?.label}</span>
<svg <ChevronDownIcon className={`h-3 w-3 shrink-0 text-gray-500 transition-transform duration-150 ${open ? "rotate-180" : ""}`} />
className={`h-3 w-3 shrink-0 text-gray-500 transition-transform duration-150 ${open ? "rotate-180" : ""}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button> </button>
); );
+3 -6
View File
@@ -7,6 +7,7 @@ import {
useRef, useRef,
useState, useState,
} from "react"; } from "react";
import { CheckIcon, ChevronRightIcon } from "../icons";
export type MenuSize = "sm" | "md"; export type MenuSize = "sm" | "md";
@@ -115,9 +116,7 @@ export function MenuItem({
<span className={`shrink-0 ${size === "sm" ? "text-[10px]" : "text-xs"} text-gray-500`}>{hint}</span> <span className={`shrink-0 ${size === "sm" ? "text-[10px]" : "text-xs"} text-gray-500`}>{hint}</span>
) : null} ) : null}
{checked ? ( {checked ? (
<svg className="h-3.5 w-3.5 shrink-0 text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <CheckIcon className="h-3.5 w-3.5 shrink-0 text-blue-400" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
</svg>
) : null} ) : null}
</button> </button>
); );
@@ -206,9 +205,7 @@ export function SubMenu({
onClick={openNow} onClick={openNow}
> >
<span className="min-w-0 flex-1 truncate text-left">{label}</span> <span className="min-w-0 flex-1 truncate text-left">{label}</span>
<svg className="h-3 w-3 shrink-0 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <ChevronRightIcon className="h-3 w-3 shrink-0 text-gray-500" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button> </button>
{open ? ( {open ? (
<div <div