snapshot of https://github.com/shadcn-ui/ui.git (main, 2026-09-15; blobs > 131072B omitted)
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
FileTextIcon,
|
||||
ImageIcon,
|
||||
MoreVerticalIcon,
|
||||
SearchIcon,
|
||||
VideoIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Badge } from "@/styles/base-sera/ui/badge"
|
||||
import { Button } from "@/styles/base-sera/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/styles/base-sera/ui/card"
|
||||
import { Checkbox } from "@/styles/base-sera/ui/checkbox"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/styles/base-sera/ui/dropdown-menu"
|
||||
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 {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/styles/base-sera/ui/table"
|
||||
|
||||
import { ASSETS, type AssetType } from "../../media-library/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 AssetTable() {
|
||||
const [selectedIds, setSelectedIds] = React.useState<Set<string>>(
|
||||
new Set(["1"])
|
||||
)
|
||||
|
||||
const toggleSelection = React.useCallback((id: string) => {
|
||||
setSelectedIds((previous) => {
|
||||
const next = new Set(previous)
|
||||
|
||||
if (next.has(id)) {
|
||||
next.delete(id)
|
||||
} else {
|
||||
next.add(id)
|
||||
}
|
||||
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<InputGroup className="w-full">
|
||||
<InputGroupAddon>
|
||||
<SearchIcon />
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput placeholder="Search files, tags, or metadata..." />
|
||||
</InputGroup>
|
||||
</CardHeader>
|
||||
<CardContent className="px-0 py-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-10 pl-6" aria-label="Select" />
|
||||
<TableHead className="w-20" aria-label="Preview" />
|
||||
<TableHead>Filename</TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Dimensions</TableHead>
|
||||
<TableHead>Size</TableHead>
|
||||
<TableHead>Uploaded By</TableHead>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead className="w-10 pr-6" aria-label="Actions" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{ASSETS.map((asset) => {
|
||||
const isSelected = selectedIds.has(asset.id)
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={asset.id}
|
||||
data-state={isSelected ? "selected" : undefined}
|
||||
className="cursor-pointer"
|
||||
onClick={() => toggleSelection(asset.id)}
|
||||
>
|
||||
<TableCell className="pl-6">
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
aria-label={`Select ${asset.name}`}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onCheckedChange={() => toggleSelection(asset.id)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="relative flex aspect-4/3 w-16 items-center justify-center bg-muted/60 ring-1 ring-border/70 ring-inset">
|
||||
{asset.duration ? (
|
||||
<span className="absolute right-1 bottom-1 bg-foreground/90 px-1 text-[0.5rem] font-semibold tracking-wider text-background">
|
||||
{asset.duration}
|
||||
</span>
|
||||
) : null}
|
||||
<AssetTypeIcon
|
||||
type={asset.type}
|
||||
className="size-4 text-muted-foreground/60"
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm font-medium text-foreground">
|
||||
{asset.name}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border px-2 py-0.5 text-[0.625rem]"
|
||||
>
|
||||
{asset.type}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">{asset.dimensions}</TableCell>
|
||||
<TableCell className="text-sm">{asset.size}</TableCell>
|
||||
<TableCell>{asset.uploadedBy}</TableCell>
|
||||
<TableCell className="text-xs font-semibold tracking-wider text-muted-foreground uppercase">
|
||||
{asset.date}
|
||||
</TableCell>
|
||||
<TableCell className="pr-6 text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={<Button variant="ghost" size="icon-xs" />}
|
||||
aria-label={`Open actions for ${asset.name}`}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>Preview</DropdownMenuItem>
|
||||
<DropdownMenuItem>Download</DropdownMenuItem>
|
||||
<DropdownMenuItem variant="destructive">
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-center py-4">
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious href="#" text="" />
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationLink href="#" isActive>
|
||||
1
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationLink href="#">2</PaginationLink>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationLink href="#">3</PaginationLink>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationNext href="#" text="" />
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { addDays, format } from "date-fns"
|
||||
import { CalendarIcon, FilterIcon, XIcon } from "lucide-react"
|
||||
import { type DateRange } from "react-day-picker"
|
||||
|
||||
import { Button } from "@/styles/base-sera/ui/button"
|
||||
import { Calendar } from "@/styles/base-sera/ui/calendar"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-sera/ui/card"
|
||||
import { Checkbox } from "@/styles/base-sera/ui/checkbox"
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxChip,
|
||||
ComboboxChips,
|
||||
ComboboxChipsInput,
|
||||
ComboboxContent,
|
||||
ComboboxEmpty,
|
||||
ComboboxItem,
|
||||
ComboboxList,
|
||||
ComboboxValue,
|
||||
useComboboxAnchor,
|
||||
} from "@/styles/base-sera/ui/combobox"
|
||||
import {
|
||||
Field,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
} from "@/styles/base-sera/ui/field"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/styles/base-sera/ui/popover"
|
||||
import { RadioGroup, RadioGroupItem } from "@/styles/base-sera/ui/radio-group"
|
||||
import { Slider } from "@/styles/base-sera/ui/slider"
|
||||
|
||||
const FILE_TYPES = [
|
||||
{
|
||||
id: "images",
|
||||
label: "Images (JPEG, PNG, WEBP)",
|
||||
defaultChecked: true,
|
||||
},
|
||||
{
|
||||
id: "video",
|
||||
label: "Video (MP4, MOV)",
|
||||
defaultChecked: false,
|
||||
},
|
||||
{
|
||||
id: "documents",
|
||||
label: "Documents (PDF)",
|
||||
defaultChecked: false,
|
||||
},
|
||||
{
|
||||
id: "audio",
|
||||
label: "Audio (MP3, WAV)",
|
||||
defaultChecked: false,
|
||||
},
|
||||
]
|
||||
|
||||
const DATE_OPTIONS = [
|
||||
{ value: "any", label: "Any time" },
|
||||
{ value: "24h", label: "Past 24 hours" },
|
||||
{ value: "week", label: "Past week" },
|
||||
{ value: "month", label: "Past month" },
|
||||
]
|
||||
|
||||
const TAGS = [
|
||||
"architecture",
|
||||
"brutalism",
|
||||
"ceramics",
|
||||
"design-week",
|
||||
"editorial",
|
||||
"exterior",
|
||||
"film",
|
||||
"food",
|
||||
"furniture",
|
||||
"interior",
|
||||
"kyoto",
|
||||
"minimalism",
|
||||
"print",
|
||||
"sustainability",
|
||||
"summer-issue",
|
||||
"video",
|
||||
] as const
|
||||
|
||||
export function FilterLibrary() {
|
||||
const tagAnchor = useComboboxAnchor()
|
||||
const [dateRange, setDateRange] = React.useState<DateRange | undefined>({
|
||||
from: new Date(new Date().getFullYear(), new Date().getMonth(), 1),
|
||||
to: addDays(
|
||||
new Date(new Date().getFullYear(), new Date().getMonth(), 1),
|
||||
21
|
||||
),
|
||||
})
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="border-b">
|
||||
<CardTitle>Filter Library</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLegend>File Type</FieldLegend>
|
||||
<Field>
|
||||
{FILE_TYPES.map((type) => (
|
||||
<Field key={type.id} orientation="horizontal">
|
||||
<Checkbox id={type.id} defaultChecked={type.defaultChecked} />
|
||||
<FieldLabel htmlFor={type.id}>{type.label}</FieldLabel>
|
||||
</Field>
|
||||
))}
|
||||
</Field>
|
||||
</FieldSet>
|
||||
<FieldSet>
|
||||
<FieldLegend>Date Uploaded</FieldLegend>
|
||||
<RadioGroup defaultValue="any">
|
||||
{DATE_OPTIONS.map((option) => (
|
||||
<Field key={option.value} orientation="horizontal">
|
||||
<RadioGroupItem
|
||||
value={option.value}
|
||||
id={`date-${option.value}`}
|
||||
/>
|
||||
<FieldLabel htmlFor={`date-${option.value}`}>
|
||||
{option.label}
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="custom-range">Custom Range</FieldLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<Button
|
||||
variant="outline"
|
||||
id="custom-range"
|
||||
className="w-full"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<CalendarIcon data-icon="inline-start" />
|
||||
{dateRange?.from ? (
|
||||
dateRange.to ? (
|
||||
<>
|
||||
{format(dateRange.from, "LLL dd, y")} –{" "}
|
||||
{format(dateRange.to, "LLL dd, y")}
|
||||
</>
|
||||
) : (
|
||||
format(dateRange.from, "LLL dd, y")
|
||||
)
|
||||
) : (
|
||||
<span>Pick a date range</span>
|
||||
)}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="end">
|
||||
<Calendar
|
||||
mode="range"
|
||||
defaultMonth={dateRange?.from}
|
||||
selected={dateRange}
|
||||
onSelect={setDateRange}
|
||||
numberOfMonths={2}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</Field>
|
||||
<FieldSet>
|
||||
<FieldLegend>File Size</FieldLegend>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between text-xs font-medium tracking-wider text-muted-foreground uppercase">
|
||||
<span>0 MB</span>
|
||||
<span>500+ MB</span>
|
||||
</div>
|
||||
<Slider defaultValue={[0, 60]} max={100} step={1} />
|
||||
<div className="flex items-center justify-between text-xs font-medium">
|
||||
<span>Min: 0 MB</span>
|
||||
<span>Max: 300 MB</span>
|
||||
</div>
|
||||
</div>
|
||||
</FieldSet>
|
||||
<FieldSet>
|
||||
<FieldLegend>Tags</FieldLegend>
|
||||
<Field>
|
||||
<Combobox
|
||||
multiple
|
||||
autoHighlight
|
||||
items={TAGS}
|
||||
defaultValue={["architecture", "brutalism"]}
|
||||
>
|
||||
<ComboboxChips ref={tagAnchor}>
|
||||
<ComboboxValue>
|
||||
{(values) => (
|
||||
<React.Fragment>
|
||||
{values.map((value: string) => (
|
||||
<ComboboxChip key={value}>{value}</ComboboxChip>
|
||||
))}
|
||||
<ComboboxChipsInput placeholder="Filter by tag..." />
|
||||
</React.Fragment>
|
||||
)}
|
||||
</ComboboxValue>
|
||||
</ComboboxChips>
|
||||
<ComboboxContent anchor={tagAnchor}>
|
||||
<ComboboxEmpty>No tags found.</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(item) => (
|
||||
<ComboboxItem key={item} value={item}>
|
||||
{item}
|
||||
</ComboboxItem>
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col gap-2 border-t">
|
||||
<Button className="w-full">Apply Filters</Button>
|
||||
<Button variant="ghost" className="w-full">
|
||||
Reset
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Separator } from "@/styles/base-sera/ui/separator"
|
||||
|
||||
import { AssetTable } from "./components/asset-table"
|
||||
import { FilterLibrary } from "./components/filter-library"
|
||||
import { PreviewHeader } from "./components/preview-header"
|
||||
|
||||
export function MediaLibraryTable() {
|
||||
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]">
|
||||
<AssetTable />
|
||||
<FilterLibrary />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user