Files
simple-react-app-kit/packages/media/src/media-grid.tsx
T
2026-09-20 16:44:15 +08:00

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>
)
}