mirror of
https://github.com/hanzoai/shadcn.git
synced 2026-10-30 18:22:59 +00:00
Upstream sync from shadcn-ui/ui (1686 commits since last merge base). Key changes: - packages/shadcn upgraded to 4.1.0 (CLI font transformers, scaffold from github, chart colors) - apps/v4 replaces apps/www as the primary docs/registry app - Font system: 25+ heading font registries, font markers utility, @supports override - Chart improvements: recharts v3 compat, radial chart fixes - templates/ updated with monorepo variants (astro, vite, react-router, start) - skills/shadcn AI skill definitions added - deprecated/ removed (auth, auth-firebase, cli, www) Preserved Hanzo-only paths: app/, pkg/, demo/, docs/, template/next/
430 lines
12 KiB
TypeScript
430 lines
12 KiB
TypeScript
"use client"
|
|
|
|
import * as React from "react"
|
|
import { Button } from "@/examples/base/ui-rtl/button"
|
|
import { Checkbox } from "@/examples/base/ui-rtl/checkbox"
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuCheckboxItem,
|
|
DropdownMenuContent,
|
|
DropdownMenuGroup,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@/examples/base/ui-rtl/dropdown-menu"
|
|
import { Input } from "@/examples/base/ui-rtl/input"
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/examples/base/ui-rtl/table"
|
|
import {
|
|
flexRender,
|
|
getCoreRowModel,
|
|
getFilteredRowModel,
|
|
getPaginationRowModel,
|
|
getSortedRowModel,
|
|
useReactTable,
|
|
type ColumnDef,
|
|
type ColumnFiltersState,
|
|
type SortingState,
|
|
type VisibilityState,
|
|
} from "@tanstack/react-table"
|
|
import { ArrowUpDown, ChevronDown, MoreHorizontal } from "lucide-react"
|
|
|
|
import {
|
|
useTranslation,
|
|
type Translations,
|
|
} from "@/components/language-selector"
|
|
|
|
const translations: Translations = {
|
|
en: {
|
|
dir: "ltr",
|
|
values: {
|
|
filterEmails: "Filter emails...",
|
|
columns: "Columns",
|
|
status: "Status",
|
|
email: "Email",
|
|
amount: "Amount",
|
|
actions: "Actions",
|
|
copyPaymentId: "Copy payment ID",
|
|
viewCustomer: "View customer",
|
|
viewPaymentDetails: "View payment details",
|
|
selectAll: "Select all",
|
|
selectRow: "Select row",
|
|
openMenu: "Open menu",
|
|
noResults: "No results.",
|
|
rowsSelected: "of",
|
|
rowsSelectedSuffix: "row(s) selected.",
|
|
previous: "Previous",
|
|
next: "Next",
|
|
success: "Success",
|
|
processing: "Processing",
|
|
failed: "Failed",
|
|
pending: "Pending",
|
|
},
|
|
},
|
|
ar: {
|
|
dir: "rtl",
|
|
values: {
|
|
filterEmails: "تصفية البريد الإلكتروني...",
|
|
columns: "الأعمدة",
|
|
status: "الحالة",
|
|
email: "البريد الإلكتروني",
|
|
amount: "المبلغ",
|
|
actions: "الإجراءات",
|
|
copyPaymentId: "نسخ معرف الدفع",
|
|
viewCustomer: "عرض العميل",
|
|
viewPaymentDetails: "عرض تفاصيل الدفع",
|
|
selectAll: "تحديد الكل",
|
|
selectRow: "تحديد الصف",
|
|
openMenu: "فتح القائمة",
|
|
noResults: "لا توجد نتائج.",
|
|
rowsSelected: "من",
|
|
rowsSelectedSuffix: "صف(وف) محدد.",
|
|
previous: "السابق",
|
|
next: "التالي",
|
|
success: "ناجح",
|
|
processing: "قيد المعالجة",
|
|
failed: "فشل",
|
|
pending: "قيد الانتظار",
|
|
},
|
|
},
|
|
he: {
|
|
dir: "rtl",
|
|
values: {
|
|
filterEmails: "סנן אימיילים...",
|
|
columns: "עמודות",
|
|
status: "סטטוס",
|
|
email: "אימייל",
|
|
amount: "סכום",
|
|
actions: "פעולות",
|
|
copyPaymentId: "העתק מזהה תשלום",
|
|
viewCustomer: "צפה בלקוח",
|
|
viewPaymentDetails: "צפה בפרטי תשלום",
|
|
selectAll: "בחר הכל",
|
|
selectRow: "בחר שורה",
|
|
openMenu: "פתח תפריט",
|
|
noResults: "אין תוצאות.",
|
|
rowsSelected: "מתוך",
|
|
rowsSelectedSuffix: "שורות נבחרו.",
|
|
previous: "הקודם",
|
|
next: "הבא",
|
|
success: "הצליח",
|
|
processing: "מעבד",
|
|
failed: "נכשל",
|
|
pending: "ממתין",
|
|
},
|
|
},
|
|
}
|
|
|
|
type Payment = {
|
|
id: string
|
|
amount: number
|
|
status: "pending" | "processing" | "success" | "failed"
|
|
email: string
|
|
}
|
|
|
|
const data: Payment[] = [
|
|
{
|
|
id: "m5gr84i9",
|
|
amount: 316,
|
|
status: "success",
|
|
email: "[email protected]",
|
|
},
|
|
{
|
|
id: "3u1reuv4",
|
|
amount: 242,
|
|
status: "success",
|
|
email: "[email protected]",
|
|
},
|
|
{
|
|
id: "derv1ws0",
|
|
amount: 837,
|
|
status: "processing",
|
|
email: "[email protected]",
|
|
},
|
|
{
|
|
id: "5kma53ae",
|
|
amount: 874,
|
|
status: "success",
|
|
email: "[email protected]",
|
|
},
|
|
{
|
|
id: "bhqecj4p",
|
|
amount: 721,
|
|
status: "failed",
|
|
email: "[email protected]",
|
|
},
|
|
]
|
|
|
|
export function DataTableRtl() {
|
|
const { t, dir, language } = useTranslation(translations, "ar")
|
|
const [sorting, setSorting] = React.useState<SortingState>([])
|
|
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
|
[]
|
|
)
|
|
const [columnVisibility, setColumnVisibility] =
|
|
React.useState<VisibilityState>({})
|
|
const [rowSelection, setRowSelection] = React.useState({})
|
|
|
|
const columns: ColumnDef<Payment>[] = React.useMemo(
|
|
() => [
|
|
{
|
|
id: "select",
|
|
header: ({ table }) => (
|
|
<Checkbox
|
|
checked={
|
|
table.getIsAllPageRowsSelected() ||
|
|
(table.getIsSomePageRowsSelected() ? true : false)
|
|
}
|
|
onCheckedChange={(value) =>
|
|
table.toggleAllPageRowsSelected(!!value)
|
|
}
|
|
aria-label={t.selectAll}
|
|
/>
|
|
),
|
|
cell: ({ row }) => (
|
|
<Checkbox
|
|
checked={row.getIsSelected()}
|
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
|
aria-label={t.selectRow}
|
|
/>
|
|
),
|
|
enableSorting: false,
|
|
enableHiding: false,
|
|
},
|
|
{
|
|
accessorKey: "status",
|
|
header: t.status,
|
|
cell: ({ row }) => {
|
|
const status = row.getValue("status") as string
|
|
const statusMap: Record<string, string> = {
|
|
success: t.success,
|
|
processing: t.processing,
|
|
failed: t.failed,
|
|
pending: t.pending,
|
|
}
|
|
return <div className="capitalize">{statusMap[status]}</div>
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "email",
|
|
header: ({ column }) => {
|
|
return (
|
|
<Button
|
|
variant="ghost"
|
|
onClick={() =>
|
|
column.toggleSorting(column.getIsSorted() === "asc")
|
|
}
|
|
>
|
|
{t.email}
|
|
<ArrowUpDown />
|
|
</Button>
|
|
)
|
|
},
|
|
cell: ({ row }) => (
|
|
<div className="lowercase">{row.getValue("email")}</div>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: "amount",
|
|
header: () => <div className="text-start">{t.amount}</div>,
|
|
cell: ({ row }) => {
|
|
const amount = parseFloat(row.getValue("amount"))
|
|
const formatted = new Intl.NumberFormat(
|
|
dir === "rtl" ? "ar-SA" : "en-US",
|
|
{
|
|
style: "currency",
|
|
currency: "USD",
|
|
}
|
|
).format(amount)
|
|
|
|
return <div className="text-start font-medium">{formatted}</div>
|
|
},
|
|
},
|
|
{
|
|
id: "actions",
|
|
enableHiding: false,
|
|
cell: ({ row }) => {
|
|
const payment = row.original
|
|
|
|
return (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger
|
|
render={<Button variant="ghost" size="icon-xs" />}
|
|
>
|
|
<span className="sr-only">{t.openMenu}</span>
|
|
<MoreHorizontal />
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent
|
|
align={dir === "rtl" ? "start" : "end"}
|
|
data-lang={dir === "rtl" ? language : undefined}
|
|
className="w-40"
|
|
>
|
|
<DropdownMenuGroup>
|
|
<DropdownMenuLabel>{t.actions}</DropdownMenuLabel>
|
|
<DropdownMenuItem
|
|
onClick={() => navigator.clipboard.writeText(payment.id)}
|
|
>
|
|
{t.copyPaymentId}
|
|
</DropdownMenuItem>
|
|
</DropdownMenuGroup>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuGroup>
|
|
<DropdownMenuItem>{t.viewCustomer}</DropdownMenuItem>
|
|
<DropdownMenuItem>{t.viewPaymentDetails}</DropdownMenuItem>
|
|
</DropdownMenuGroup>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)
|
|
},
|
|
},
|
|
],
|
|
[t, dir, language]
|
|
)
|
|
|
|
const table = useReactTable({
|
|
data,
|
|
columns,
|
|
onSortingChange: setSorting,
|
|
onColumnFiltersChange: setColumnFilters,
|
|
getCoreRowModel: getCoreRowModel(),
|
|
getPaginationRowModel: getPaginationRowModel(),
|
|
getSortedRowModel: getSortedRowModel(),
|
|
getFilteredRowModel: getFilteredRowModel(),
|
|
onColumnVisibilityChange: setColumnVisibility,
|
|
onRowSelectionChange: setRowSelection,
|
|
state: {
|
|
sorting,
|
|
columnFilters,
|
|
columnVisibility,
|
|
rowSelection,
|
|
},
|
|
})
|
|
|
|
return (
|
|
<div className="w-full">
|
|
<div className="flex items-center gap-2 py-4">
|
|
<Input
|
|
placeholder={t.filterEmails}
|
|
value={(table.getColumn("email")?.getFilterValue() as string) ?? ""}
|
|
onChange={(event) =>
|
|
table.getColumn("email")?.setFilterValue(event.target.value)
|
|
}
|
|
className="max-w-sm"
|
|
/>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger
|
|
render={<Button variant="outline" className="ms-auto" />}
|
|
>
|
|
{t.columns} <ChevronDown />
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent
|
|
align={dir === "rtl" ? "start" : "end"}
|
|
data-lang={dir === "rtl" ? language : undefined}
|
|
>
|
|
<DropdownMenuGroup>
|
|
{table
|
|
.getAllColumns()
|
|
.filter((column) => column.getCanHide())
|
|
.map((column) => {
|
|
return (
|
|
<DropdownMenuCheckboxItem
|
|
key={column.id}
|
|
className="capitalize"
|
|
checked={column.getIsVisible()}
|
|
onCheckedChange={(value) =>
|
|
column.toggleVisibility(!!value)
|
|
}
|
|
>
|
|
{column.id}
|
|
</DropdownMenuCheckboxItem>
|
|
)
|
|
})}
|
|
</DropdownMenuGroup>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
<div className="overflow-hidden rounded-md border">
|
|
<Table>
|
|
<TableHeader>
|
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
<TableRow key={headerGroup.id}>
|
|
{headerGroup.headers.map((header) => {
|
|
return (
|
|
<TableHead key={header.id}>
|
|
{header.isPlaceholder
|
|
? null
|
|
: flexRender(
|
|
header.column.columnDef.header,
|
|
header.getContext()
|
|
)}
|
|
</TableHead>
|
|
)
|
|
})}
|
|
</TableRow>
|
|
))}
|
|
</TableHeader>
|
|
<TableBody>
|
|
{table.getRowModel().rows?.length ? (
|
|
table.getRowModel().rows.map((row) => (
|
|
<TableRow
|
|
key={row.id}
|
|
data-state={row.getIsSelected() && "selected"}
|
|
>
|
|
{row.getVisibleCells().map((cell) => (
|
|
<TableCell key={cell.id}>
|
|
{flexRender(
|
|
cell.column.columnDef.cell,
|
|
cell.getContext()
|
|
)}
|
|
</TableCell>
|
|
))}
|
|
</TableRow>
|
|
))
|
|
) : (
|
|
<TableRow>
|
|
<TableCell
|
|
colSpan={columns.length}
|
|
className="h-24 text-center"
|
|
>
|
|
{t.noResults}
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
<div className="flex items-center justify-end gap-2 py-4">
|
|
<div className="flex-1 text-sm text-muted-foreground">
|
|
{table.getFilteredSelectedRowModel().rows.length} {t.rowsSelected}{" "}
|
|
{table.getFilteredRowModel().rows.length} {t.rowsSelectedSuffix}
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => table.previousPage()}
|
|
disabled={!table.getCanPreviousPage()}
|
|
>
|
|
{t.previous}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => table.nextPage()}
|
|
disabled={!table.getCanNextPage()}
|
|
>
|
|
{t.next}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|