103 lines
3.6 KiB
TypeScript
103 lines
3.6 KiB
TypeScript
import { CheckIcon, FilmIcon, HeartIcon } from "@workspace/ui/components/icon"
|
|
import { useTranslate } from "@workspace/i18n"
|
|
import { cn } from "@workspace/ui/lib/utils"
|
|
|
|
import { mediaMessages } from "./messages"
|
|
import type { MediaAsset } from "./types"
|
|
|
|
export interface MediaGridProps {
|
|
activeId?: MediaAsset["id"]
|
|
assets: readonly MediaAsset[]
|
|
resolveUrl: (asset: MediaAsset) => string
|
|
selectedIds: ReadonlySet<MediaAsset["id"]>
|
|
onActivate: (asset: MediaAsset) => void
|
|
onDetails: (asset: MediaAsset) => void
|
|
onToggle?: (asset: MediaAsset) => void
|
|
}
|
|
|
|
export function MediaGrid({
|
|
activeId,
|
|
assets,
|
|
resolveUrl,
|
|
selectedIds,
|
|
onActivate,
|
|
onDetails,
|
|
onToggle,
|
|
}: MediaGridProps) {
|
|
const t = useTranslate()
|
|
|
|
return (
|
|
<div className="columns-2 gap-3 p-3 sm:columns-3 lg:columns-4 2xl:columns-5">
|
|
{assets.map((asset) => {
|
|
const selected = selectedIds.has(asset.id)
|
|
const active = activeId === asset.id
|
|
|
|
return (
|
|
<button
|
|
key={asset.id}
|
|
type="button"
|
|
data-media-id={asset.id}
|
|
aria-label={`${t(mediaMessages.select)} ${asset.name}`}
|
|
aria-pressed={selected}
|
|
onClick={() => {
|
|
onActivate(asset)
|
|
onToggle?.(asset)
|
|
}}
|
|
onDoubleClick={() => onDetails(asset)}
|
|
className={cn(
|
|
"group relative mb-3 block w-full break-inside-avoid overflow-hidden rounded-lg bg-muted text-left ring-offset-2 ring-offset-background transition-shadow outline-none focus-visible:ring-3 focus-visible:ring-ring",
|
|
(active || selected) && "ring-2 ring-primary"
|
|
)}
|
|
>
|
|
{asset.kind === "image" ? (
|
|
<img
|
|
src={resolveUrl(asset)}
|
|
alt={asset.name}
|
|
loading="lazy"
|
|
className="block min-h-24 w-full bg-muted object-cover"
|
|
style={
|
|
asset.width && asset.height
|
|
? { aspectRatio: `${asset.width} / ${asset.height}` }
|
|
: undefined
|
|
}
|
|
/>
|
|
) : (
|
|
<div
|
|
className="relative aspect-video min-h-28 bg-black"
|
|
style={
|
|
asset.width && asset.height
|
|
? { aspectRatio: `${asset.width} / ${asset.height}` }
|
|
: undefined
|
|
}
|
|
>
|
|
<video
|
|
src={resolveUrl(asset)}
|
|
muted
|
|
preload="metadata"
|
|
className="size-full object-cover"
|
|
/>
|
|
<span className="absolute inset-0 grid place-items-center bg-black/10">
|
|
<FilmIcon className="size-8 text-white drop-shadow-md" />
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
<span className="absolute inset-x-0 bottom-0 flex items-end justify-between gap-2 bg-linear-to-t from-black/75 to-transparent px-2 pt-8 pb-2 text-white opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100">
|
|
<span className="min-w-0 truncate text-xs">{asset.name}</span>
|
|
{asset.favorite && (
|
|
<HeartIcon className="size-3.5 shrink-0 fill-current" />
|
|
)}
|
|
</span>
|
|
|
|
{selected && (
|
|
<span className="absolute top-2 right-2 grid size-6 place-items-center rounded-full bg-primary text-primary-foreground shadow">
|
|
<CheckIcon className="size-4" strokeWidth={3} />
|
|
</span>
|
|
)}
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|