layered
// over it, never nested inside the anchor: a button inside an anchor is invalid HTML and swallows the
// keyboard behaviour of both.
//
// That sibling relationship is why `group` belongs on the WRAPPER. It sat on the overlay link instead, which
// the checkbox is not inside, so `group-hover` could never have reached it — and the checkbox fell back to
// its own `hover`, revealing itself only when the pointer was already over the invisible 20px circle. The
// affordance existed and was unfindable.
type AssetTileProps = {
asset: GridAsset;
token: string | null;
width: number;
height: number;
selected: boolean;
/** True once anything is selected — the checkbox stops being hover-only and the click selects, not opens. */
selecting: boolean;
onToggle: (id: string, ev: React.MouseEvent) => void;
};
export const AssetTile = ({ asset, token, width, height, selected, selecting, onToggle }: AssetTileProps) => {
const [params] = useSearchParams();
const [loaded, setLoaded] = useState(false);
const duration = formatDuration(asset.duration);
const href = (() => {
const next = new URLSearchParams(params);
next.set(ASSET_PARAM, asset.id);
return `?${next.toString()}`;
})();
const badges = (
<>
{asset.isFavorite && }
{asset.stackCount > 1 && (
{asset.stackCount}
)}
{!asset.isImage && (
{duration ?? ''}
)}
>
);
const inner = (
<>
{/* A neutral block until the thumbnail decodes, so the row doesn't flash white on a dark page. */}
setLoaded(true)}
className={`h-full w-full object-cover transition-opacity duration-200 ${loaded ? 'opacity-100' : 'opacity-0'}`}
/>
{badges}
>
);
return (
{selecting ? (
// Once a selection exists, a plain click extends it rather than navigating — matching Immich, where
// you can rubber-band through a month without the lightbox opening on every tile.
onToggle(asset.id, ev)}
className="absolute inset-0 block h-full w-full cursor-pointer"
>
{inner}
) : (
{inner}
)}
onToggle(asset.id, ev)}
title={selected ? 'Deselect' : 'Select'}
className={`absolute left-1.5 top-1.5 rounded-full text-white transition-opacity ${
selected ? 'opacity-100' : 'opacity-0 group-hover:opacity-100 focus-visible:opacity-100'
} ${selecting ? 'opacity-100' : ''}`}
>
{selected ? (
) : (
)}
{selected && }
);
};