feat(media): extract workspace package
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user