snapshot of https://github.com/shadcn-ui/ui.git (main, 2026-09-15; blobs > 131072B omitted)
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import {
|
||||
DownloadIcon,
|
||||
FileTextIcon,
|
||||
ImageIcon,
|
||||
PlusIcon,
|
||||
VideoIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Badge } from "@/styles/base-sera/ui/badge"
|
||||
import { Button } from "@/styles/base-sera/ui/button"
|
||||
import { Card, CardContent, CardFooter } from "@/styles/base-sera/ui/card"
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemTitle,
|
||||
} from "@/styles/base-sera/ui/item"
|
||||
import { Separator } from "@/styles/base-sera/ui/separator"
|
||||
|
||||
import { type Asset, type AssetType } from "../data"
|
||||
|
||||
const TYPE_LABEL: Record<AssetType, string> = {
|
||||
JPEG: "Image / JPEG",
|
||||
PNG: "Image / PNG",
|
||||
WEBP: "Image / WEBP",
|
||||
MP4: "Video / MP4",
|
||||
PDF: "Document / PDF",
|
||||
}
|
||||
|
||||
export function AssetDetails({ asset }: { asset: Asset }) {
|
||||
return (
|
||||
<Card className="gap-0">
|
||||
<CardContent className="flex flex-col gap-6">
|
||||
<div className="flex aspect-5/4 items-center justify-center bg-muted/60 text-muted-foreground/60 ring-1 ring-border/70 ring-inset">
|
||||
{asset.type === "MP4" ? (
|
||||
<VideoIcon className="size-8" />
|
||||
) : asset.type === "PDF" ? (
|
||||
<FileTextIcon className="size-8" />
|
||||
) : (
|
||||
<ImageIcon className="size-8" />
|
||||
)}
|
||||
</div>
|
||||
<h2 className="line-clamp-2 font-heading text-xl tracking-wide">
|
||||
{asset.name}
|
||||
</h2>
|
||||
<Separator />
|
||||
<dl className="flex flex-col gap-5 text-sm">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<dt className="text-[0.625rem] font-semibold tracking-widest text-muted-foreground uppercase">
|
||||
Asset Type
|
||||
</dt>
|
||||
<dd>{TYPE_LABEL[asset.type]}</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<dt className="text-[0.625rem] font-semibold tracking-widest text-muted-foreground uppercase">
|
||||
Dimensions
|
||||
</dt>
|
||||
<dd>{asset.dimensions}</dd>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<dt className="text-[0.625rem] font-semibold tracking-widest text-muted-foreground uppercase">
|
||||
File Size
|
||||
</dt>
|
||||
<dd>{asset.size}</dd>
|
||||
</div>
|
||||
</div>
|
||||
</dl>
|
||||
<Separator />
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-[0.625rem] font-semibold tracking-widest text-muted-foreground uppercase">
|
||||
Tags
|
||||
</h3>
|
||||
<Button variant="ghost" size="icon-xs" aria-label="Add tag">
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-2">
|
||||
{asset.tags.map((tag) => (
|
||||
<Badge key={tag}>{tag}</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex flex-col gap-3">
|
||||
<h3 className="text-[0.625rem] font-semibold tracking-widest text-muted-foreground uppercase">
|
||||
Used In
|
||||
</h3>
|
||||
<div className="flex flex-col gap-2">
|
||||
{asset.usedIn.map((usage) => (
|
||||
<Item key={usage.title} variant="outline">
|
||||
<ItemContent>
|
||||
<ItemTitle>{usage.title}</ItemTitle>
|
||||
<ItemDescription>{usage.role}</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="mt-6 border-t pt-6">
|
||||
<Button className="w-full">
|
||||
<DownloadIcon data-icon="inline-start" />
|
||||
Download
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
"use client"
|
||||
|
||||
import { cn } from "cn"
|
||||
import {
|
||||
CheckIcon,
|
||||
FileTextIcon,
|
||||
ImageIcon,
|
||||
SearchIcon,
|
||||
VideoIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Badge } from "@/styles/base-sera/ui/badge"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/styles/base-sera/ui/card"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/styles/base-sera/ui/input-group"
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from "@/styles/base-sera/ui/pagination"
|
||||
|
||||
import { ASSETS, type Asset, type AssetType } from "../data"
|
||||
|
||||
function AssetTypeIcon({
|
||||
type,
|
||||
className,
|
||||
}: {
|
||||
type: AssetType
|
||||
className?: string
|
||||
}) {
|
||||
if (type === "MP4") {
|
||||
return <VideoIcon className={className} />
|
||||
}
|
||||
|
||||
if (type === "PDF") {
|
||||
return <FileTextIcon className={className} />
|
||||
}
|
||||
|
||||
return <ImageIcon className={className} />
|
||||
}
|
||||
|
||||
export function AssetGrid({
|
||||
selectedId,
|
||||
onSelect,
|
||||
}: {
|
||||
selectedId: string
|
||||
onSelect: (id: string) => void
|
||||
}) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<InputGroup className="w-full">
|
||||
<InputGroupAddon>
|
||||
<SearchIcon />
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput placeholder="Search files, tags, or metadata..." />
|
||||
</InputGroup>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-6 sm:grid-cols-3 lg:grid-cols-4">
|
||||
{ASSETS.map((asset) => (
|
||||
<AssetGridItem
|
||||
key={asset.id}
|
||||
asset={asset}
|
||||
selected={asset.id === selectedId}
|
||||
onSelect={() => onSelect(asset.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-center">
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious href="#" />
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationLink href="#" isActive>
|
||||
1
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationLink href="#">2</PaginationLink>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationLink href="#">3</PaginationLink>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationNext href="#" />
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
function AssetGridItem({
|
||||
asset,
|
||||
selected,
|
||||
onSelect,
|
||||
}: {
|
||||
asset: Asset
|
||||
selected: boolean
|
||||
onSelect: () => void
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSelect}
|
||||
aria-pressed={selected}
|
||||
className="group flex flex-col gap-2.5 text-left outline-none focus-visible:[&>div:first-child]:ring-2 focus-visible:[&>div:first-child]:ring-ring"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex aspect-4/3 items-center justify-center bg-muted/60 ring-1 ring-border/70 transition-shadow ring-inset group-hover:ring-foreground/40",
|
||||
selected && "ring-2 ring-foreground group-hover:ring-foreground"
|
||||
)}
|
||||
>
|
||||
{selected ? (
|
||||
<div className="absolute top-2 left-2 flex size-5 items-center justify-center bg-foreground text-background">
|
||||
<CheckIcon className="size-3" />
|
||||
</div>
|
||||
) : null}
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="absolute top-2 right-2 border bg-background px-2 py-1 text-[0.625rem]"
|
||||
>
|
||||
{asset.type}
|
||||
</Badge>
|
||||
{asset.duration ? (
|
||||
<Badge className="absolute bottom-2 left-2 bg-foreground px-2 py-1 text-background">
|
||||
{asset.duration}
|
||||
</Badge>
|
||||
) : null}
|
||||
<AssetTypeIcon
|
||||
type={asset.type}
|
||||
className="size-7 text-muted-foreground/60"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 px-0.5">
|
||||
<p className="line-clamp-1 text-sm font-medium">{asset.name}</p>
|
||||
<p className="text-[0.625rem] font-semibold tracking-wider text-muted-foreground uppercase">
|
||||
{asset.date} · {asset.size}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ArrowLeftIcon, SlidersHorizontalIcon, UploadIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
} from "@/styles/base-sera/ui/breadcrumb"
|
||||
import { Button } from "@/styles/base-sera/ui/button"
|
||||
import { ButtonGroup } from "@/styles/base-sera/ui/button-group"
|
||||
|
||||
export function PreviewHeader() {
|
||||
return (
|
||||
<header>
|
||||
<div className="container flex flex-col items-center justify-center gap-(--gap) py-(--gap) sm:flex-row sm:justify-between">
|
||||
<div className="flex flex-col gap-2 text-center sm:text-left">
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="#" className="flex items-center gap-1.5">
|
||||
<ArrowLeftIcon className="size-3.5" />
|
||||
Asset management
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<h1 className="line-clamp-1 font-heading text-3xl tracking-wide uppercase md:text-3xl lg:text-4xl">
|
||||
Media Library
|
||||
</h1>
|
||||
</div>
|
||||
<ButtonGroup className="gap-2 sm:ml-auto md:gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="bg-background hover:bg-background/80"
|
||||
>
|
||||
<SlidersHorizontalIcon data-icon="inline-start" />
|
||||
Filters
|
||||
</Button>
|
||||
<Button>
|
||||
<UploadIcon data-icon="inline-start" />
|
||||
Upload Assets
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
188
apps/v4/app/(app)/(styles)/sera/media-library/data.ts
Normal file
188
apps/v4/app/(app)/(styles)/sera/media-library/data.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
export type AssetType = "JPEG" | "PNG" | "WEBP" | "MP4" | "PDF"
|
||||
|
||||
export type Asset = {
|
||||
id: string
|
||||
name: string
|
||||
date: string
|
||||
size: string
|
||||
type: AssetType
|
||||
dimensions: string
|
||||
duration?: string
|
||||
uploadedBy: string
|
||||
uploadedByInitials: string
|
||||
uploadedOn: string
|
||||
tags: string[]
|
||||
usedIn: { title: string; role: string }[]
|
||||
}
|
||||
|
||||
export const ASSETS: Asset[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "brutalism-facade-01.jpg",
|
||||
date: "Oct 24",
|
||||
size: "4.2 MB",
|
||||
type: "JPEG",
|
||||
dimensions: "4000 × 3000",
|
||||
uploadedBy: "Marcus Chen",
|
||||
uploadedByInitials: "MC",
|
||||
uploadedOn: "Oct 24, 2024",
|
||||
tags: ["architecture", "brutalism", "exterior", "summer-issue"],
|
||||
usedIn: [
|
||||
{ title: "Brutalism's Second Act", role: "Cover Image" },
|
||||
{ title: "Autumn Sartorial Code", role: "Inline Gallery" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "brutalism-interior-raw.jpg",
|
||||
date: "Oct 24",
|
||||
size: "3.8 MB",
|
||||
type: "JPEG",
|
||||
dimensions: "3800 × 2850",
|
||||
uploadedBy: "Marcus Chen",
|
||||
uploadedByInitials: "MC",
|
||||
uploadedOn: "Oct 24, 2024",
|
||||
tags: ["architecture", "brutalism", "interior"],
|
||||
usedIn: [{ title: "Brutalism's Second Act", role: "Inline Gallery" }],
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "seattle-living-building-diagram.png",
|
||||
date: "Oct 22",
|
||||
size: "1.1 MB",
|
||||
type: "PNG",
|
||||
dimensions: "2000 × 1500",
|
||||
uploadedBy: "Sarah Jenkins",
|
||||
uploadedByInitials: "SJ",
|
||||
uploadedOn: "Oct 22, 2024",
|
||||
tags: ["diagram", "sustainability", "seattle"],
|
||||
usedIn: [
|
||||
{ title: "The Future of Sustainable Architecture", role: "Diagram" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "interview-sofia-coppola-clip1.mp4",
|
||||
date: "Oct 18",
|
||||
size: "45.0 MB",
|
||||
type: "MP4",
|
||||
dimensions: "1920 × 1080",
|
||||
duration: "0:45",
|
||||
uploadedBy: "Emma Ross",
|
||||
uploadedByInitials: "ER",
|
||||
uploadedOn: "Oct 18, 2024",
|
||||
tags: ["video", "interview", "film"],
|
||||
usedIn: [{ title: "The Aesthetics of Isolation", role: "Featured Video" }],
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
name: "kyoto-kilns-pottery-detail.jpg",
|
||||
date: "Oct 15",
|
||||
size: "5.6 MB",
|
||||
type: "JPEG",
|
||||
dimensions: "4500 × 3000",
|
||||
uploadedBy: "Marcus Chen",
|
||||
uploadedByInitials: "MC",
|
||||
uploadedOn: "Oct 15, 2024",
|
||||
tags: ["ceramics", "kyoto", "craft"],
|
||||
usedIn: [{ title: "Kyoto's Oldest Kilns", role: "Hero Image" }],
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
name: "copenhagen-design-week-street.jpg",
|
||||
date: "Oct 12",
|
||||
size: "3.2 MB",
|
||||
type: "JPEG",
|
||||
dimensions: "3600 × 2400",
|
||||
uploadedBy: "Noah Bennett",
|
||||
uploadedByInitials: "NB",
|
||||
uploadedOn: "Oct 12, 2024",
|
||||
tags: ["copenhagen", "design-week", "street"],
|
||||
usedIn: [{ title: "Field Notes from Copenhagen", role: "Inline Gallery" }],
|
||||
},
|
||||
{
|
||||
id: "7",
|
||||
name: "minimalist-chair-render.webp",
|
||||
date: "Oct 10",
|
||||
size: "0.8 MB",
|
||||
type: "WEBP",
|
||||
dimensions: "2400 × 1600",
|
||||
uploadedBy: "Claire Duval",
|
||||
uploadedByInitials: "CD",
|
||||
uploadedOn: "Oct 10, 2024",
|
||||
tags: ["furniture", "minimalism", "render"],
|
||||
usedIn: [{ title: "The New Vanguard", role: "Product Shot" }],
|
||||
},
|
||||
{
|
||||
id: "8",
|
||||
name: "autumn-issue-style-guide.pdf",
|
||||
date: "Oct 05",
|
||||
size: "12.4 MB",
|
||||
type: "PDF",
|
||||
dimensions: "N/A",
|
||||
uploadedBy: "Emma Ross",
|
||||
uploadedByInitials: "ER",
|
||||
uploadedOn: "Oct 05, 2024",
|
||||
tags: ["guidelines", "internal", "autumn"],
|
||||
usedIn: [{ title: "Autumn Issue 2024", role: "Reference" }],
|
||||
},
|
||||
{
|
||||
id: "9",
|
||||
name: "milan-lighting-studio-visit.jpg",
|
||||
date: "Oct 09",
|
||||
size: "6.1 MB",
|
||||
type: "JPEG",
|
||||
dimensions: "5200 × 3466",
|
||||
uploadedBy: "Claire Duval",
|
||||
uploadedByInitials: "CD",
|
||||
uploadedOn: "Oct 09, 2024",
|
||||
tags: ["milan", "lighting", "studio"],
|
||||
usedIn: [
|
||||
{ title: "Milan's Most Elusive Lighting Designer", role: "Hero Image" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "10",
|
||||
name: "lisbon-culinary-scene-raw.webp",
|
||||
date: "Oct 14",
|
||||
size: "2.4 MB",
|
||||
type: "WEBP",
|
||||
dimensions: "3000 × 2000",
|
||||
uploadedBy: "Amara Iqbal",
|
||||
uploadedByInitials: "AI",
|
||||
uploadedOn: "Oct 14, 2024",
|
||||
tags: ["lisbon", "food", "editorial"],
|
||||
usedIn: [
|
||||
{ title: "Lisbon's Quiet Culinary Renaissance", role: "Inline Gallery" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "11",
|
||||
name: "print-magazine-covers-mo...",
|
||||
date: "Sep 26",
|
||||
size: "8.9 MB",
|
||||
type: "PNG",
|
||||
dimensions: "3200 × 2400",
|
||||
uploadedBy: "Mina Okafor",
|
||||
uploadedByInitials: "MO",
|
||||
uploadedOn: "Sep 26, 2024",
|
||||
tags: ["print", "magazine", "covers"],
|
||||
usedIn: [{ title: "The Return of Print", role: "Cover Image" }],
|
||||
},
|
||||
{
|
||||
id: "12",
|
||||
name: "avant-garde-furniture-trailer.mp4",
|
||||
date: "Sep 30",
|
||||
size: "78.2 MB",
|
||||
type: "MP4",
|
||||
dimensions: "3840 × 2160",
|
||||
duration: "1:12",
|
||||
uploadedBy: "Tommy Rhodes",
|
||||
uploadedByInitials: "TR",
|
||||
uploadedOn: "Sep 30, 2024",
|
||||
tags: ["video", "furniture", "trailer"],
|
||||
usedIn: [
|
||||
{ title: "Collecting the New Avant-Garde", role: "Featured Video" },
|
||||
],
|
||||
},
|
||||
]
|
||||
30
apps/v4/app/(app)/(styles)/sera/media-library/index.tsx
Normal file
30
apps/v4/app/(app)/(styles)/sera/media-library/index.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Separator } from "@/styles/base-sera/ui/separator"
|
||||
|
||||
import { AssetDetails } from "./components/asset-details"
|
||||
import { AssetGrid } from "./components/asset-grid"
|
||||
import { PreviewHeader } from "./components/preview-header"
|
||||
import { ASSETS } from "./data"
|
||||
|
||||
export function MediaLibrary() {
|
||||
const [selectedId, setSelectedId] = React.useState<string>(ASSETS[0].id)
|
||||
|
||||
const selectedAsset = React.useMemo(
|
||||
() => ASSETS.find((asset) => asset.id === selectedId) ?? ASSETS[0],
|
||||
[selectedId]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="preview theme-taupe @container/preview w-full flex-1 bg-muted pt-4 font-sans ring-1 ring-foreground/5 [--gap:--spacing(4)] sm:pt-0 md:[--gap:--spacing(6)] xl:[--gap:--spacing(8)] 2xl:py-8 **:[.container]:px-(--gap)">
|
||||
<PreviewHeader />
|
||||
<Separator className="hidden sm:block" />
|
||||
<div className="container grid grid-cols-1 items-start gap-(--gap) py-(--gap) xl:grid-cols-[minmax(0,1fr)_320px]">
|
||||
<AssetGrid selectedId={selectedId} onSelect={setSelectedId} />
|
||||
<AssetDetails asset={selectedAsset} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user