snapshot of https://github.com/shadcn-ui/ui.git (main, 2026-09-15; blobs > 131072B omitted)
All checks were successful
Release / Create Version PR or Publish Stable Release (push) Has been skipped
Release / Publish Prerelease to NPM (push) Has been skipped
Monitor Registries / Monitor registry health (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-15 22:44:43 +05:00
commit 1416db152c
5727 changed files with 630238 additions and 0 deletions

View File

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

View File

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

View File

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

View 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" },
],
},
]

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