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:
@@ -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<string, WorkerKey> = {
|
||||
Thumbnails: "thumbnail",
|
||||
@@ -43,10 +44,7 @@ interface FailedWorkerItem {
|
||||
function FailedWorkerItemRow({ item }: { item: FailedWorkerItem }) {
|
||||
return (
|
||||
<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">
|
||||
<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>
|
||||
<WarningIcon className="mt-px h-2.5 w-2.5 shrink-0 text-amber-500 light-theme:text-amber-700" strokeWidth={2.5} />
|
||||
<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>
|
||||
{item.error && (
|
||||
@@ -386,9 +384,7 @@ export function BackgroundTasks() {
|
||||
onClick={(e) => { e.stopPropagation(); toggleWorker(primary.id, workerKey); }}
|
||||
>
|
||||
{isPaused ? (
|
||||
<svg className="h-2.5 w-2.5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
<PlayIcon className="h-2.5 w-2.5" />
|
||||
) : (
|
||||
<svg className="h-2.5 w-2.5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
|
||||
@@ -448,12 +444,7 @@ export function BackgroundTasks() {
|
||||
|
||||
{/* Expand chevron (only when multiple tasks) */}
|
||||
{allTasks.length > 1 && (
|
||||
<svg
|
||||
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>
|
||||
<ChevronDownIcon className={`h-3.5 w-3.5 text-gray-600 transition-transform duration-200 shrink-0 ${expanded ? "rotate-180" : ""}`} />
|
||||
)}
|
||||
|
||||
{/* 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); }}
|
||||
>
|
||||
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<CloseIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
@@ -518,9 +507,7 @@ export function BackgroundTasks() {
|
||||
onClick={() => toggleWorker(task.id, workerKey)}
|
||||
>
|
||||
{isPaused ? (
|
||||
<svg className="h-2.5 w-2.5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
<PlayIcon className="h-2.5 w-2.5" />
|
||||
) : (
|
||||
<svg className="h-2.5 w-2.5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<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"
|
||||
onClick={() => dismissTask(task.id, task.snapshot)}
|
||||
>
|
||||
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<CloseIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
>
|
||||
<svg className="h-4 w-4" 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>
|
||||
<StarIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</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"
|
||||
>
|
||||
<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">
|
||||
<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>
|
||||
<WarningIcon className="h-3.5 w-3.5 shrink-0" />
|
||||
<p className="text-xs font-semibold">Delete from disk</p>
|
||||
</div>
|
||||
<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"
|
||||
onClick={clearGallerySelection}
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<CloseIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@@ -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() {
|
||||
<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="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">
|
||||
<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>
|
||||
<WarningIcon className="h-3.5 w-3.5 shrink-0" />
|
||||
<p className="text-xs font-semibold">Delete from disk</p>
|
||||
</div>
|
||||
<p className="mb-2.5 text-[11px] leading-relaxed text-gray-400">
|
||||
|
||||
@@ -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}`}
|
||||
>
|
||||
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<CheckIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
|
||||
<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"
|
||||
onClick={onNavigate}
|
||||
>
|
||||
<svg className={`h-4 w-4 ${entry.has_children ? "" : "opacity-45"}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<ChevronRightIcon className={`h-4 w-4 ${entry.has_children ? "" : "opacity-45"}`} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -203,9 +200,7 @@ function StagedFoldersPanel({
|
||||
onClick={() => onRemove(path)}
|
||||
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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<CloseIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</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"
|
||||
onClick={() => setFolderPickerOpen(false)}
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<CloseIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@@ -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"
|
||||
}`}
|
||||
>
|
||||
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<CheckIcon className="h-3 w-3" strokeWidth={3} />
|
||||
</div>
|
||||
</button>
|
||||
{/* 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">
|
||||
{image.media_kind === "video" ? (
|
||||
<svg className="h-7 w-7" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
<PlayIcon className="h-7 w-7" />
|
||||
) : (
|
||||
<svg className="h-7 w-7" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
<PhotoIcon className="h-7 w-7" strokeWidth={1} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -124,9 +118,7 @@ export function ImageTile({
|
||||
{image.media_kind === "video" && (
|
||||
<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">
|
||||
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
<PlayIcon className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -136,10 +128,7 @@ export function ImageTile({
|
||||
{image.embedding_status === "failed" && (
|
||||
<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">
|
||||
<svg className="h-2.5 w-2.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
<WarningIcon className="h-2.5 w-2.5 shrink-0" strokeWidth={2.5} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
@@ -153,9 +142,7 @@ export function ImageTile({
|
||||
{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">
|
||||
{Array.from({ length: image.rating }, (_, index) => (
|
||||
<svg key={index} className="h-2.5 w-2.5" 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>
|
||||
<StarIcon key={index} className="h-2.5 w-2.5" />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -176,9 +163,7 @@ export function ImageTile({
|
||||
{image.rating > 0 ? (
|
||||
<div className="flex items-center gap-0.5">
|
||||
{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">
|
||||
<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>
|
||||
<StarIcon key={i} className="h-2.5 w-2.5 text-amber-300" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
@@ -338,10 +323,7 @@ export function Gallery() {
|
||||
) : 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="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">
|
||||
<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>
|
||||
<PhotoIcon className="h-12 w-12 mx-auto text-white/10 mb-4" strokeWidth={0.75} />
|
||||
<p className="text-sm text-white/30 font-medium">
|
||||
{imageLoadError
|
||||
? "Could not load results"
|
||||
|
||||
@@ -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(); }}
|
||||
>
|
||||
<svg
|
||||
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>
|
||||
<StarIcon className={`h-4 w-4 ${rating <= image.rating ? "text-amber-300" : "text-white/20 hover:text-white/40"}`} />
|
||||
</button>
|
||||
</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"
|
||||
onClick={async () => { await updateImageDetails(image.id, { rating: 0 }); onClose(); }}
|
||||
>
|
||||
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<CloseIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
|
||||
@@ -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();
|
||||
}}
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<CloseIcon className="h-4 w-4" strokeWidth={1.8} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</motion.div>
|
||||
@@ -811,9 +810,7 @@ export function Lightbox() {
|
||||
goSlideshow(1);
|
||||
}}
|
||||
>
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<ChevronRightIcon className="h-5 w-5" strokeWidth={1.8} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -1006,9 +1003,7 @@ export function Lightbox() {
|
||||
</Tooltip>
|
||||
</div>
|
||||
<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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<CloseIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1042,9 +1037,7 @@ export function Lightbox() {
|
||||
</span>
|
||||
) : regionSelectMode ? (
|
||||
<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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<CloseIcon className="h-3 w-3" />
|
||||
Cancel selection
|
||||
</span>
|
||||
) : (
|
||||
@@ -1074,13 +1067,7 @@ export function Lightbox() {
|
||||
className="rounded-md p-1"
|
||||
onClick={() => void updateImageDetails(selectedImage.id, { rating })}
|
||||
>
|
||||
<svg
|
||||
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>
|
||||
<StarIcon className={`h-5 w-5 ${rating <= selectedImage.rating ? "text-amber-300" : "text-white/20 hover:text-white/50"}`} />
|
||||
</button>
|
||||
</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"
|
||||
onClick={() => void updateImageDetails(selectedImage.id, { rating: 0 })}
|
||||
>
|
||||
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<CloseIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
@@ -1211,9 +1196,7 @@ export function Lightbox() {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<CloseIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</span>
|
||||
@@ -1426,9 +1409,7 @@ export function Lightbox() {
|
||||
goNext();
|
||||
}}
|
||||
>
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<ChevronRightIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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)}
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<CloseIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@@ -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({
|
||||
</svg>
|
||||
</span>
|
||||
) : (
|
||||
<svg className="w-3.5 h-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
<FolderIcon className="w-3.5 h-3.5 shrink-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"
|
||||
onClick={(e) => { e.stopPropagation(); setConfirmingRemoval(true); }}
|
||||
>
|
||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.75}
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<CloseIcon className="w-3 h-3" strokeWidth={1.75} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -334,9 +329,7 @@ function AlbumItem({
|
||||
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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<CheckIcon className="h-2.5 w-2.5" strokeWidth={3} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -369,10 +362,7 @@ function AlbumItem({
|
||||
<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">
|
||||
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
<PhotoIcon className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -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"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.75} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -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"
|
||||
>
|
||||
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.75} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
<PlusIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@@ -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();
|
||||
}}
|
||||
>
|
||||
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<CloseIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
<svg
|
||||
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>
|
||||
<ChevronRightIcon className={`h-3 w-3 shrink-0 text-gray-600 transition-transform ${open ? "rotate-90" : ""}`} strokeWidth={2.5} />
|
||||
<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>
|
||||
</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"
|
||||
onClick={closeWhatsNew}
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<CloseIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@@ -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)}
|
||||
>
|
||||
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<CloseIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</span>
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<CloseIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@@ -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,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<T> {
|
||||
value: T;
|
||||
@@ -85,14 +86,7 @@ export function Dropdown<T extends string | number | null>({
|
||||
>
|
||||
{triggerIcon}
|
||||
<span className="min-w-0 truncate">{current?.label}</span>
|
||||
<svg
|
||||
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>
|
||||
<ChevronDownIcon className={`h-3 w-3 shrink-0 text-gray-500 transition-transform duration-150 ${open ? "rotate-180" : ""}`} />
|
||||
</button>
|
||||
);
|
||||
|
||||
|
||||
@@ -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({
|
||||
<span className={`shrink-0 ${size === "sm" ? "text-[10px]" : "text-xs"} text-gray-500`}>{hint}</span>
|
||||
) : null}
|
||||
{checked ? (
|
||||
<svg className="h-3.5 w-3.5 shrink-0 text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<CheckIcon className="h-3.5 w-3.5 shrink-0 text-blue-400" />
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
@@ -206,9 +205,7 @@ export function SubMenu({
|
||||
onClick={openNow}
|
||||
>
|
||||
<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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<ChevronRightIcon className="h-3 w-3 shrink-0 text-gray-500" />
|
||||
</button>
|
||||
{open ? (
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user