mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
docs(design-system): Data Table and Data Grid documentation (#41252)
* basic differentiation * docs * data-grid examples * data-grid-demo * data-table * demo * use existing components * improvements * markup * docs * remove data table * lint * Update apps/design-system/content/docs/ui-patterns/empty-states.mdx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * fix ref * use TanStack sorting * grammar * dependency * 📝 Add docstrings to `dnywh/docs/data-table-data-grid` (#41255) Docstrings generation was requested by @MildTomato. * https://github.com/supabase/supabase/pull/41252#issuecomment-3640781017 The following files were modified: * `apps/design-system/registry/default/example/data-grid-demo.tsx` * `apps/design-system/registry/default/example/data-grid-empty-state.tsx` * `apps/design-system/registry/default/example/data-table-demo.tsx` Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
3d6be390a6
commit
ce294e3fcf
12 files changed
+798
-869
No files matched your search
@@ -2722,13 +2722,35 @@ export const Index: Record<string, any> = {
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"empty-state-zero-items-data-grid": {
|
||||
name: "empty-state-zero-items-data-grid",
|
||||
"data-grid-demo": {
|
||||
name: "data-grid-demo",
|
||||
type: "components:example",
|
||||
registryDependencies: undefined,
|
||||
component: React.lazy(() => import("@/registry/default/example/empty-state-zero-items-data-grid")),
|
||||
component: React.lazy(() => import("@/registry/default/example/data-grid-demo")),
|
||||
source: "",
|
||||
files: ["registry/default/example/empty-state-zero-items-data-grid.tsx"],
|
||||
files: ["registry/default/example/data-grid-demo.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"data-grid-empty-state": {
|
||||
name: "data-grid-empty-state",
|
||||
type: "components:example",
|
||||
registryDependencies: undefined,
|
||||
component: React.lazy(() => import("@/registry/default/example/data-grid-empty-state")),
|
||||
source: "",
|
||||
files: ["registry/default/example/data-grid-empty-state.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"data-table-demo": {
|
||||
name: "data-table-demo",
|
||||
type: "components:example",
|
||||
registryDependencies: ["table"],
|
||||
component: React.lazy(() => import("@/registry/default/example/data-table-demo")),
|
||||
source: "",
|
||||
files: ["registry/default/example/data-table-demo.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
|
||||
@@ -283,11 +283,6 @@ export const docsConfig: DocsConfig = {
|
||||
href: '/docs/components/context-menu',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: 'Data Table',
|
||||
href: '/docs/components/data-table',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: 'Date Picker',
|
||||
href: '/docs/components/date-picker',
|
||||
|
||||
@@ -1,842 +0,0 @@
|
||||
---
|
||||
title: Data Table
|
||||
description: Powerful table and datagrids built using TanStack Table.
|
||||
component: true
|
||||
links:
|
||||
doc: https://tanstack.com/table/v8/docs/guide/introduction
|
||||
---
|
||||
|
||||
<ComponentPreview name="data-table-demo" peekCode wide />
|
||||
|
||||
## Introduction
|
||||
|
||||
Every data table or datagrid I've created has been unique. They all behave differently, have specific sorting and filtering requirements, and work with different data sources.
|
||||
|
||||
It doesn't make sense to combine all of these variations into a single component. If we do that, we'll lose the flexibility that [headless UI](https://tanstack.com/table/v8/docs/guide/introduction#what-is-headless-ui) provides.
|
||||
|
||||
So instead of a data-table component, I thought it would be more helpful to provide a guide on how to build your own.
|
||||
|
||||
We'll start with the basic `<Table />` component and build a complex data table from scratch.
|
||||
|
||||
<Callout className="mt-4">
|
||||
|
||||
**Tip:** If you find yourself using the same table in multiple places in your app, you can always extract it into a reusable component.
|
||||
|
||||
</Callout>
|
||||
|
||||
## Table of Contents
|
||||
|
||||
This guide will show you how to use [TanStack Table](https://tanstack.com/table) and the `<Table />` component to build your own custom data table. We'll cover the following topics:
|
||||
|
||||
- [Basic Table](#basic-table)
|
||||
- [Row Actions](#row-actions)
|
||||
- [Pagination](#pagination)
|
||||
- [Sorting](#sorting)
|
||||
- [Filtering](#filtering)
|
||||
- [Visibility](#visibility)
|
||||
- [Row Selection](#row-selection)
|
||||
- [Reusable Components](#reusable-components)
|
||||
|
||||
## Installation
|
||||
|
||||
1. Add the `<Table />` component to your project:
|
||||
|
||||
```bash
|
||||
npx shadcn-ui@latest add table
|
||||
```
|
||||
|
||||
2. Add `tanstack/react-table` dependency:
|
||||
|
||||
```bash
|
||||
npm install @tanstack/react-table
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
We are going to build a table to show recent payments. Here's what our data looks like:
|
||||
|
||||
```tsx showLineNumbers
|
||||
type Payment = {
|
||||
id: string
|
||||
amount: number
|
||||
status: 'pending' | 'processing' | 'success' | 'failed'
|
||||
email: string
|
||||
}
|
||||
|
||||
export const payments: Payment[] = [
|
||||
{
|
||||
id: '728ed52f',
|
||||
amount: 100,
|
||||
status: 'pending',
|
||||
email: 'm@example.com',
|
||||
},
|
||||
{
|
||||
id: '489e1d42',
|
||||
amount: 125,
|
||||
status: 'processing',
|
||||
email: 'example@gmail.com',
|
||||
},
|
||||
// ...
|
||||
]
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
Start by creating the following file structure:
|
||||
|
||||
```txt
|
||||
app
|
||||
└── payments
|
||||
├── columns.tsx
|
||||
├── data-table.tsx
|
||||
└── page.tsx
|
||||
```
|
||||
|
||||
I'm using a Next.js example here but this works for any other React framework.
|
||||
|
||||
- `columns.tsx` (client component) will contain our column definitions.
|
||||
- `data-table.tsx` (client component) will contain our `<DataTable />` component.
|
||||
- `page.tsx` (server component) is where we'll fetch data and render our table.
|
||||
|
||||
## Basic Table
|
||||
|
||||
Let's start by building a basic table.
|
||||
|
||||
<Steps>
|
||||
|
||||
### Column Definitions
|
||||
|
||||
First, we'll define our columns.
|
||||
|
||||
```tsx showLineNumbers title="app/payments/columns.tsx" {3,14-27}
|
||||
'use client'
|
||||
|
||||
import { ColumnDef } from '@tanstack/react-table'
|
||||
|
||||
// This type is used to define the shape of our data.
|
||||
// You can use a Zod schema here if you want.
|
||||
export type Payment = {
|
||||
id: string
|
||||
amount: number
|
||||
status: 'pending' | 'processing' | 'success' | 'failed'
|
||||
email: string
|
||||
}
|
||||
|
||||
export const columns: ColumnDef<Payment>[] = [
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: 'Status',
|
||||
},
|
||||
{
|
||||
accessorKey: 'email',
|
||||
header: 'Email',
|
||||
},
|
||||
{
|
||||
accessorKey: 'amount',
|
||||
header: 'Amount',
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
<Callout className="mt-4">
|
||||
|
||||
**Note:** Columns are where you define the core of what your table
|
||||
will look like. They define the data that will be displayed, how it will be
|
||||
formatted, sorted and filtered.
|
||||
|
||||
</Callout>
|
||||
|
||||
### `<DataTable />` component
|
||||
|
||||
Next, we'll create a `<DataTable />` component to render our table.
|
||||
|
||||
```tsx showLineNumbers title="app/payments/data-table.tsx"
|
||||
'use client'
|
||||
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[]
|
||||
data: TData[]
|
||||
}
|
||||
|
||||
export function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="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">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
<Callout>
|
||||
|
||||
**Tip**: If you find yourself using `<DataTable />` in multiple places, this is the component you could make reusable by extracting it to `components/ui/data-table.tsx`.
|
||||
|
||||
`<DataTable columns={columns} data={data} />`
|
||||
|
||||
</Callout>
|
||||
|
||||
### Render the table
|
||||
|
||||
Finally, we'll render our table in our page component.
|
||||
|
||||
```tsx showLineNumbers title="app/payments/page.tsx" {22}
|
||||
import { Payment, columns } from './columns'
|
||||
import { DataTable } from './data-table'
|
||||
|
||||
async function getData(): Promise<Payment[]> {
|
||||
// Fetch data from your API here.
|
||||
return [
|
||||
{
|
||||
id: '728ed52f',
|
||||
amount: 100,
|
||||
status: 'pending',
|
||||
email: 'm@example.com',
|
||||
},
|
||||
// ...
|
||||
]
|
||||
}
|
||||
|
||||
export default async function DemoPage() {
|
||||
const data = await getData()
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-10">
|
||||
<DataTable columns={columns} data={data} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</Steps>
|
||||
|
||||
## Cell Formatting
|
||||
|
||||
Let's format the amount cell to display the dollar amount. We'll also align the cell to the right.
|
||||
|
||||
<Steps>
|
||||
|
||||
### Update columns definition
|
||||
|
||||
Update the `header` and `cell` definitions for amount as follows:
|
||||
|
||||
```tsx showLineNumbers title="app/payments/columns.tsx" {4-15}
|
||||
export const columns: ColumnDef<Payment>[] = [
|
||||
{
|
||||
accessorKey: 'amount',
|
||||
header: () => <div className="text-right">Amount</div>,
|
||||
cell: ({ row }) => {
|
||||
const amount = parseFloat(row.getValue('amount'))
|
||||
const formatted = new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
}).format(amount)
|
||||
|
||||
return <div className="text-right font-medium">{formatted}</div>
|
||||
},
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
You can use the same approach to format other cells and headers.
|
||||
|
||||
</Steps>
|
||||
|
||||
## Row Actions
|
||||
|
||||
Let's add row actions to our table. We'll use a `<Dropdown />` component for this.
|
||||
|
||||
<Steps>
|
||||
|
||||
### Update columns definition
|
||||
|
||||
Update our columns definition to add a new `actions` column. The `actions` cell returns a `<Dropdown />` component.
|
||||
|
||||
```tsx showLineNumbers title="app/payments/columns.tsx" {4,6-14,18-45}
|
||||
'use client'
|
||||
|
||||
import { ColumnDef } from '@tanstack/react-table'
|
||||
import { MoreHorizontal } from 'lucide-react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
|
||||
export const columns: ColumnDef<Payment>[] = [
|
||||
// ...
|
||||
{
|
||||
id: 'actions',
|
||||
cell: ({ row }) => {
|
||||
const payment = row.original
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuItem onClick={() => navigator.clipboard.writeText(payment.id)}>
|
||||
Copy payment ID
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>View customer</DropdownMenuItem>
|
||||
<DropdownMenuItem>View payment details</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
},
|
||||
},
|
||||
// ...
|
||||
]
|
||||
```
|
||||
|
||||
You can access the row data using `row.original` in the `cell` function. Use this to handle actions for your row eg. use the `id` to make a DELETE call to your API.
|
||||
|
||||
</Steps>
|
||||
|
||||
## Pagination
|
||||
|
||||
Next, we'll add pagination to our table.
|
||||
|
||||
<Steps>
|
||||
|
||||
### Update `<DataTable>`
|
||||
|
||||
```tsx showLineNumbers title="app/payments/data-table.tsx" {5,17}
|
||||
import {
|
||||
ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table'
|
||||
|
||||
export function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
})
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
This will automatically paginate your rows into pages of 10. See the [pagination docs](https://tanstack.com/table/v8/docs/api/features/pagination) for more information on customizing page size and implementing manual pagination.
|
||||
|
||||
### Add pagination controls
|
||||
|
||||
We can add pagination controls to our table using the `<Button />` component and the `table.previousPage()`, `table.nextPage()` API methods.
|
||||
|
||||
```tsx showLineNumbers title="app/payments/data-table.tsx" {1,15,21-39}
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
export function DataTable<TData, TValue>({
|
||||
columns,
|
||||
data,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
{ // .... }
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex items-center justify-end space-x-2 py-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
See [Reusable Components](#reusable-components) section for a more advanced pagination component.
|
||||
|
||||
</Steps>
|
||||
|
||||
## Sorting
|
||||
|
||||
Let's make the email column sortable.
|
||||
|
||||
<Steps>
|
||||
|
||||
### Update `<DataTable>`
|
||||
|
||||
```tsx showLineNumbers title="app/payments/data-table.tsx" showLineNumbers {3,6,10,18,25-28}
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
ColumnDef,
|
||||
SortingState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
|
||||
export function DataTable<TData, TValue>({
|
||||
columns,
|
||||
data,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const [sorting, setSorting] = React.useState<SortingState>([])
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onSortingChange: setSorting,
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
state: {
|
||||
sorting,
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="rounded-md border">
|
||||
<Table>{ ... }</Table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Make header cell sortable
|
||||
|
||||
We can now update the `email` header cell to add sorting controls.
|
||||
|
||||
```tsx showLineNumbers title="app/payments/columns.tsx" {4,9-19}
|
||||
'use client'
|
||||
|
||||
import { ColumnDef } from '@tanstack/react-table'
|
||||
import { ArrowUpDown, MoreHorizontal } from 'lucide-react'
|
||||
|
||||
export const columns: ColumnDef<Payment>[] = [
|
||||
{
|
||||
accessorKey: 'email',
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||
>
|
||||
Email
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
This will automatically sort the table (asc and desc) when the user toggles on the header cell.
|
||||
|
||||
</Steps>
|
||||
|
||||
## Filtering
|
||||
|
||||
Let's add a search input to filter emails in our table.
|
||||
|
||||
<Steps>
|
||||
|
||||
### Update `<DataTable>`
|
||||
|
||||
```tsx showLineNumbers title="app/payments/data-table.tsx" {6,10,17,24-26,35-36,39,45-54}
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
SortingState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
|
||||
export function DataTable<TData, TValue>({
|
||||
columns,
|
||||
data,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const [sorting, setSorting] = React.useState<SortingState>([])
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
[]
|
||||
)
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
onSortingChange: setSorting,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center py-4">
|
||||
<Input
|
||||
placeholder="Filter emails..."
|
||||
value={(table.getColumn("email")?.getFilterValue() as string) ?? ""}
|
||||
onChange={(event) =>
|
||||
table.getColumn("email")?.setFilterValue(event.target.value)
|
||||
}
|
||||
className="max-w-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="rounded-md border">
|
||||
<Table>{ ... }</Table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Filtering is now enabled for the `email` column. You can add filters to other columns as well. See the [filtering docs](https://tanstack.com/table/v8/docs/guide/filters) for more information on customizing filters.
|
||||
|
||||
</Steps>
|
||||
|
||||
## Visibility
|
||||
|
||||
Adding column visibility is fairly simple using `@tanstack/react-table` visibility API.
|
||||
|
||||
<Steps>
|
||||
|
||||
### Update `<DataTable>`
|
||||
|
||||
```tsx showLineNumbers title="app/payments/data-table.tsx" {8,18-23,33-34,45,49,64-91}
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
SortingState,
|
||||
VisibilityState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
|
||||
export function DataTable<TData, TValue>({
|
||||
columns,
|
||||
data,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const [sorting, setSorting] = React.useState<SortingState>([])
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
[]
|
||||
)
|
||||
const [columnVisibility, setColumnVisibility] =
|
||||
React.useState<VisibilityState>({})
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
columnVisibility,
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center py-4">
|
||||
<Input
|
||||
placeholder="Filter emails..."
|
||||
value={table.getColumn("email")?.getFilterValue() as string}
|
||||
onChange={(event) =>
|
||||
table.getColumn("email")?.setFilterValue(event.target.value)
|
||||
}
|
||||
className="max-w-sm"
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="ml-auto">
|
||||
Columns
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{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>
|
||||
)
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<div className="rounded-md border">
|
||||
<Table>{ ... }</Table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
This adds a dropdown menu that you can use to toggle column visibility.
|
||||
|
||||
</Steps>
|
||||
|
||||
## Row Selection
|
||||
|
||||
Next, we're going to add row selection to our table.
|
||||
|
||||
<Steps>
|
||||
|
||||
### Update column definitions
|
||||
|
||||
```tsx showLineNumbers title="app/payments/columns.tsx" {6,9-27}
|
||||
'use client'
|
||||
|
||||
import { ColumnDef } from '@tanstack/react-table'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
|
||||
export const columns: ColumnDef<Payment>[] = [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && 'indeterminate')
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="Select row"
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
### Update `<DataTable>`
|
||||
|
||||
```tsx showLineNumbers title="app/payments/data-table.tsx" {11,23,28}
|
||||
export function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
|
||||
const [sorting, setSorting] = React.useState<SortingState>([])
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
|
||||
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({})
|
||||
const [rowSelection, setRowSelection] = React.useState({})
|
||||
|
||||
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>
|
||||
<div className="rounded-md border">
|
||||
<Table />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
This adds a checkbox to each row and a checkbox in the header to select all rows.
|
||||
|
||||
### Show selected rows
|
||||
|
||||
You can show the number of selected rows using the `table.getFilteredSelectedRowModel()` API.
|
||||
|
||||
```tsx
|
||||
<div className="flex-1 text-sm text-muted-foreground">
|
||||
{table.getFilteredSelectedRowModel().rows.length} of {table.getFilteredRowModel().rows.length}{' '}
|
||||
row(s) selected.
|
||||
</div>
|
||||
```
|
||||
|
||||
</Steps>
|
||||
|
||||
## Reusable Components
|
||||
|
||||
Here are some components you can use to build your data tables. This is from the [Tasks](/examples/tasks) demo.
|
||||
|
||||
### Column header
|
||||
|
||||
Make any column header sortable and hideable.
|
||||
|
||||
<ComponentSource src="/app/(app)/examples/tasks/components/data-table-column-header.tsx" />
|
||||
|
||||
```tsx {5}
|
||||
export const columns = [
|
||||
{
|
||||
accessorKey: 'email',
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title="Email" />,
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
### Pagination
|
||||
|
||||
Add pagination controls to your table including page size and selection count.
|
||||
|
||||
<ComponentSource src="/app/(app)/examples/tasks/components/data-table-pagination.tsx" />
|
||||
|
||||
```tsx
|
||||
<DataTablePagination table={table} />
|
||||
```
|
||||
|
||||
### Column toggle
|
||||
|
||||
A component to toggle column visibility.
|
||||
|
||||
<ComponentSource src="/app/(app)/examples/tasks/components/data-table-view-options.tsx" />
|
||||
|
||||
```tsx
|
||||
<DataTableViewOptions table={table} />
|
||||
```
|
||||
@@ -215,6 +215,6 @@ This hybrid approach requires careful attention to event handling. Ensure that t
|
||||
|
||||
## Related
|
||||
|
||||
Use the Table component for simple, static tabular data presentation with a fixed number of rows. Consider using [Data Table](../components/data-table) for more complex cases as it provides built-in sorting, filtering, and pagination capabilities.
|
||||
Use the Table component for simple, static tabular data presentation with a fixed number of rows. Consider using [Data Table](../ui-patterns/tables#data-table) for more complex cases as it provides built-in sorting, filtering, and pagination capabilities.
|
||||
|
||||
Refer to [Tables](../ui-patterns/tables) for broader guidance and best practices on presenting tabular data.
|
||||
@@ -42,13 +42,13 @@ A [Table](../components/table) instance with zero results should display a singl
|
||||
|
||||
<ComponentPreview name="empty-state-zero-items-table" peekCode wide />
|
||||
|
||||
#### DataGrid
|
||||
#### Data Grid
|
||||
|
||||
[DataGrid](../components/data-table) typically spans the full height and width of a container. A classic example is [Users](https://supabase.com/dashboard/project/_/auth/users), which (as it sounds) displays a list of the project’s registered users. Any instance with zero results should display a more prominent empty with a clear title, description, and supporting illustration.
|
||||
[Data Grid](../ui-patterns/tables#data-grid) and [Data Table](../ui-patterns/tables#data-table) component patterns typically span the full height and width of a container. A classic example is [Users](https://supabase.com/dashboard/project/_/auth/users), which (as it sounds) displays a list of the project’s registered users. Any instance with zero results should display a more prominent empty with a clear title, description, and supporting illustration.
|
||||
|
||||
<ComponentPreview name="empty-state-zero-items-data-grid" peekCode wide />
|
||||
<ComponentPreview name="data-grid-empty-state" peekCode wide />
|
||||
|
||||
Other DataGrid instances include [Cron Jobs](https://supabase.com/dashboard/project/_/integrations/cron/jobs) and [Queues](https://supabase.com/dashboard/project/_/integrations/queues).
|
||||
Other Data Grid instances include [Cron Jobs](https://supabase.com/dashboard/project/_/integrations/cron/jobs) and [Queues](https://supabase.com/dashboard/project/_/integrations/queues).
|
||||
|
||||
## Missing route
|
||||
|
||||
@@ -61,7 +61,3 @@ Users may accidentally navigate to a non-existent dynamic route, such as a non-e
|
||||
For presentational empty states (initial states with value propositions and actions), use the [EmptyStatePresentational](../fragments/empty-state-presentational) component from `ui-patterns`. This component provides a consistent structure with support for icons, titles, descriptions, and action buttons.
|
||||
|
||||
For other empty state scenarios (zero results, missing routes, etc), custom components may still be appropriate as the context and needs for each placement can differ significantly.
|
||||
|
||||
## External references
|
||||
|
||||
- [_Empty States_ on GitHub Primer](https://primer.style/product/ui-patterns/empty-states/)
|
||||
@@ -5,26 +5,79 @@ description: Display structured data in a scannable, organized way.
|
||||
|
||||
Tables are a fundamental pattern for displaying structured data in rows and columns. They provide a scannable, organized way to present collections of related information, making it easy for users to compare values, identify patterns, and take action on specific items.
|
||||
|
||||
The choice of table component depends on several factors: the complexity of the data, the level of interactivity required, the amount of data being displayed, and the context within the page layout.
|
||||
The choice of table pattern depends on several factors: the complexity of the data, the level of interactivity required, the amount of data being displayed, and the context within the page layout.
|
||||
|
||||
## Components
|
||||
|
||||
There are two main table components, each optimized for different use cases:
|
||||
There are three main table patterns, each suited to different use cases:
|
||||
|
||||
- [Table](../components/table) is a low-level, presentational table component.
|
||||
- [Data Table](#data-table) builds on [Table](../components/table) and [TanStack Table](https://tanstack.com/table) to provide a feature-rich data browsing experience (sorting, filtering, pagination, etc.).
|
||||
- [Data Grid](#data-grid) is a separate grid implementation used for highly interactive, spreadsheet-like surfaces and very large datasets.
|
||||
|
||||
Use Table when:
|
||||
|
||||
- You need simple, static display
|
||||
- No filtering or complex behavior is needed
|
||||
|
||||
Use Data Table when:
|
||||
|
||||
- You need sorting, pagination, filtering, search, or row actions
|
||||
- You want TanStack-powered behavior with table semantics
|
||||
|
||||
Use Data Grid when:
|
||||
|
||||
- You need virtualization today
|
||||
- You need column resizing
|
||||
- You need spreadsheet-like editing
|
||||
|
||||
Data Table and Data Grid are both _pattern components_: they are composed from primitives and built per use case. They are not available as standalone components.
|
||||
|
||||
### Table
|
||||
|
||||
The [Table](../components/table) component is designed for simple, static tabular data presentation. Use it when:
|
||||
[Table](../components/table) is designed for simple, static tabular data presentation. It is a presentational wrapper around the HTML `<table>` element. Use it when:
|
||||
|
||||
- Displaying a fixed, known number of rows
|
||||
- The data is primarily read-only
|
||||
- Sort, filter, or search actions are not required or can be basic
|
||||
|
||||
<ComponentPreview name="table-demo" peekCode wide />
|
||||
|
||||
### Data Table
|
||||
|
||||
The [Data Table](../components/data-table) component is designed for immersive, data-heavy experiences. Use it when:
|
||||
Data Table is a pattern component and is not exposed as a Design System component. It is built on top of [Table](../components/table) and [TanStack Table](https://tanstack.com/table).
|
||||
|
||||
Data Table extends Table with column definitions and row models (via TanStack) for complex sorting, filtering, and row actions. Use it when:
|
||||
|
||||
- Displaying large datasets that require pagination
|
||||
- Users need to perform complex sort, filter, or search actions through the data
|
||||
- Row selection is required
|
||||
- The table is the primary focus of the page
|
||||
- Full-viewport layouts are appropriate
|
||||
|
||||
Data Table does not yet support virtualization, resizable columns, or advanced editors. These capabilities are planned as part of [consolidation with Data Grid](#future).
|
||||
|
||||
<ComponentPreview name="data-table-demo" peekCode wide />
|
||||
|
||||
As you can see from the above example, Data Table’s composition is heavily dependent on use case. We do not yet have a shared Design System component for this reason.
|
||||
|
||||
Follow [Shadcn’s Data Table documentation](https://ui.shadcn.com/docs/components/data-table) for a complete guide on building upon the Data Table pattern for each specific use case. These patterns map closely to the approaches we follow internally.
|
||||
|
||||
### Data Grid
|
||||
|
||||
Data Grid is a pattern component and is not exposed as a Design System component. It is based on [React Data Grid](https://comcast.github.io/react-data-grid/#/CommonFeatures) and originally adopted for areas including Studio’s Table Editor, Query Performance, and other high-interaction surfaces.
|
||||
|
||||
Use it only when you need virtualization, column resizing, or complex cell editing. Otherwise [Data Table](#data-table) is simpler and more flexible.
|
||||
|
||||
<ComponentPreview name="data-grid-demo" peekCode wide />
|
||||
|
||||
## Future
|
||||
|
||||
Data Table and Data Grid overlap significantly. We’re looking at consolidating these into one data table component, which will likely be improvements to Data Table given the numerous advantages of TanStack Table and difficulties extending React Data Grid.
|
||||
|
||||
The likely direction is a single Data Table component built on TanStack Table, with
|
||||
|
||||
- Virtualization
|
||||
- Resizable columns
|
||||
- Plug-in cell editors
|
||||
- Shared filtering/sorting utilities
|
||||
- Shared UI patterns (horizontal and vertical filter bars, date pickers, side panels)
|
||||
- Accessible, semantic HTML table markup
|
||||
@@ -18,6 +18,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.1.1",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"contentlayer2": "0.4.6",
|
||||
"date-fns": "^2.30.0",
|
||||
"dayjs": "1.11.13",
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import { useState } from 'react'
|
||||
import DataGrid, { Column, useRowSelection } from 'react-data-grid'
|
||||
import 'react-data-grid/lib/styles.css'
|
||||
import { Checkbox_Shadcn_, cn } from 'ui'
|
||||
|
||||
type User = {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
phone: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a data grid demo with selectable rows and sample user data.
|
||||
*
|
||||
* Renders a DataGrid configured with a checkbox column for per-row selection, columns for display name,
|
||||
* email, and phone, and ten hard-coded sample users. Selection state is managed internally and applied
|
||||
* to row styling.
|
||||
*
|
||||
* @returns A React element that renders the configured DataGrid with row selection and sample rows.
|
||||
*/
|
||||
export default function DataGridDemo() {
|
||||
const [selectedRows, setSelectedRows] = useState<Set<string>>(new Set())
|
||||
|
||||
const columns: Column<User>[] = [
|
||||
{
|
||||
key: 'checkbox',
|
||||
name: '',
|
||||
width: 50,
|
||||
resizable: false,
|
||||
headerCellClass: 'border-default border-r border-b',
|
||||
renderCell: ({ row }) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const [isRowSelected, onRowSelectionChange] = useRowSelection()
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Checkbox_Shadcn_
|
||||
checked={isRowSelected}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onRowSelectionChange({
|
||||
row,
|
||||
type: 'ROW',
|
||||
checked: !isRowSelected,
|
||||
isShiftClick: e.shiftKey,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
name: 'Display name',
|
||||
minWidth: 200,
|
||||
resizable: true,
|
||||
headerCellClass: 'border-default border-r border-b',
|
||||
},
|
||||
{
|
||||
key: 'email',
|
||||
name: 'Email',
|
||||
minWidth: 250,
|
||||
resizable: true,
|
||||
headerCellClass: 'border-default border-r border-b',
|
||||
},
|
||||
{
|
||||
key: 'phone',
|
||||
name: 'Phone',
|
||||
minWidth: 150,
|
||||
resizable: true,
|
||||
headerCellClass: 'border-default border-b',
|
||||
},
|
||||
]
|
||||
|
||||
const rows: User[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Wallace',
|
||||
email: 'wallace@example.com',
|
||||
phone: '+44 1234 567890',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Gromit',
|
||||
email: 'gromit@example.com',
|
||||
phone: '+44 1234 567891',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Wendolene Ramsbottom',
|
||||
email: 'wendolene@example.com',
|
||||
phone: '+44 1234 567892',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Feathers McGraw',
|
||||
email: 'feathers@example.com',
|
||||
phone: '+44 1234 567893',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Preston',
|
||||
email: 'preston@example.com',
|
||||
phone: '+44 1234 567894',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
name: 'Piella Bakewell',
|
||||
email: 'piella@example.com',
|
||||
phone: '+44 1234 567895',
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
name: 'Victor Quartermaine',
|
||||
email: 'victor@example.com',
|
||||
phone: '+44 1234 567896',
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
name: 'Lady Tottington',
|
||||
email: 'lady@example.com',
|
||||
phone: '+44 1234 567897',
|
||||
},
|
||||
{
|
||||
id: '9',
|
||||
name: 'Shaun',
|
||||
email: 'shaun@example.com',
|
||||
phone: '+44 1234 567898',
|
||||
},
|
||||
{
|
||||
id: '10',
|
||||
name: 'Hutch',
|
||||
email: 'hutch@example.com',
|
||||
phone: '+44 1234 567899',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="h-full w-full flex flex-col relative min-h-[400px] rounded-md border overflow-hidden">
|
||||
<DataGrid
|
||||
className="flex-grow border-t-0 bg-dash-canvas"
|
||||
rowHeight={44}
|
||||
headerRowHeight={36}
|
||||
columns={columns}
|
||||
rows={rows}
|
||||
rowKeyGetter={(row: User) => row.id}
|
||||
rowClass={(row, idx) => {
|
||||
const isSelected = selectedRows.has(row.id)
|
||||
const isLastRow = idx === rows.length - 1
|
||||
return cn(
|
||||
'bg-surface-75',
|
||||
isSelected && 'bg-surface-200',
|
||||
'[&>.rdg-cell]:border-box [&>.rdg-cell]:outline-none [&>.rdg-cell]:shadow-none',
|
||||
'[&>.rdg-cell]:border-secondary [&>.rdg-cell:not(:last-child)]:border-r',
|
||||
!isLastRow && '[&>.rdg-cell]:border-b',
|
||||
'[&>.rdg-cell:nth-child(2)>div]:ml-8'
|
||||
)
|
||||
}}
|
||||
selectedRows={selectedRows}
|
||||
onSelectedRowsChange={setSelectedRows}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+8
-1
@@ -3,7 +3,14 @@ import DataGrid, { Column } from 'react-data-grid'
|
||||
import 'react-data-grid/lib/styles.css'
|
||||
import { cn } from 'ui'
|
||||
|
||||
export default function EmptyStateZeroItemsDataGrid() {
|
||||
/**
|
||||
* Renders a DataGrid configured to display an empty "no users" state.
|
||||
*
|
||||
* The grid includes three columns (Display name, Email, Phone) and no rows, and supplies a centered overlay with an icon and explanatory text when there are no rows.
|
||||
*
|
||||
* @returns A React element containing the configured DataGrid with an empty-state fallback UI.
|
||||
*/
|
||||
export default function DataGridEmptyState() {
|
||||
const columns: Column<{ id: string; name: string; email: string }>[] = [
|
||||
{ key: 'name', name: 'Display name', minWidth: 200, resizable: true },
|
||||
{ key: 'email', name: 'Email', minWidth: 250, resizable: true },
|
||||
@@ -0,0 +1,349 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
SortingState,
|
||||
useReactTable,
|
||||
VisibilityState,
|
||||
} from '@tanstack/react-table'
|
||||
import { ChevronDown, MoreVertical } from 'lucide-react'
|
||||
import * as React from 'react'
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Checkbox_Shadcn_,
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
Input,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableHeadSort,
|
||||
TableRow,
|
||||
} from 'ui'
|
||||
|
||||
const data: Payment[] = [
|
||||
{
|
||||
id: 'm5gr84i9',
|
||||
amount: 316,
|
||||
status: 'success',
|
||||
email: 'wallace@example.com',
|
||||
},
|
||||
{
|
||||
id: '3u1reuv4',
|
||||
amount: 242,
|
||||
status: 'success',
|
||||
email: 'wendolene@example.com',
|
||||
},
|
||||
{
|
||||
id: 'derv1ws0',
|
||||
amount: 837,
|
||||
status: 'processing',
|
||||
email: 'piella@example.com',
|
||||
},
|
||||
{
|
||||
id: '5kma53ae',
|
||||
amount: 874,
|
||||
status: 'success',
|
||||
email: 'victor@example.com',
|
||||
},
|
||||
{
|
||||
id: 'bhqecj4p',
|
||||
amount: 721,
|
||||
status: 'failed',
|
||||
email: 'feathers@example.com',
|
||||
},
|
||||
]
|
||||
|
||||
export type Payment = {
|
||||
id: string
|
||||
amount: number
|
||||
status: 'pending' | 'processing' | 'success' | 'failed'
|
||||
email: string
|
||||
}
|
||||
|
||||
export const columns: ColumnDef<Payment>[] = [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
<Checkbox_Shadcn_
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() ? 'indeterminate' : false)
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox_Shadcn_
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="Select row"
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: 'Status',
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => <div className="capitalize">{row.getValue('status')}</div>,
|
||||
},
|
||||
{
|
||||
accessorKey: 'email',
|
||||
header: 'Email',
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => <div className="lowercase">{row.getValue('email')}</div>,
|
||||
},
|
||||
{
|
||||
accessorKey: 'amount',
|
||||
header: () => <div className="text-right">Amount</div>,
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const amount = parseFloat(row.getValue('amount'))
|
||||
|
||||
// Format the amount as a dollar amount
|
||||
const formatted = new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
}).format(amount)
|
||||
|
||||
return <div className="text-right">{formatted}</div>
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
enableHiding: false,
|
||||
header: () => <span className="sr-only">Actions</span>,
|
||||
cell: ({ row }) => {
|
||||
const payment = row.original
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="default" className="px-1.5" icon={<MoreVertical />} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="max-w-48">
|
||||
<DropdownMenuItem onClick={() => navigator.clipboard.writeText(payment.id)}>
|
||||
Copy payment ID
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>View customer</DropdownMenuItem>
|
||||
<DropdownMenuItem>View payment details</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Demo React component showcasing a client-side data table with sorting, filtering, column visibility, row selection, and pagination controls.
|
||||
*
|
||||
* Renders a fully interactive table UI bound to local state and TanStack Table: filter input for email, column visibility dropdown, sortable headers with three-state cycling, per-row selection checkboxes, row action menu, and previous/next pagination controls.
|
||||
*
|
||||
* @returns A React element that renders the interactive data table demo.
|
||||
*/
|
||||
export default function DataTableDemo() {
|
||||
const [sorting, setSorting] = React.useState<SortingState>([])
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
|
||||
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({})
|
||||
const [rowSelection, setRowSelection] = React.useState({})
|
||||
|
||||
// Convert TanStack Table's SortingState to the string format expected by TableHeadSort
|
||||
const getSortString = React.useMemo(() => {
|
||||
if (sorting.length === 0) return ''
|
||||
const sort = sorting[0]
|
||||
return `${sort.id}:${sort.desc ? 'desc' : 'asc'}`
|
||||
}, [sorting])
|
||||
|
||||
// Handle sort changes from TableHeadSort and convert to TanStack Table's SortingState
|
||||
const handleSortChange = React.useCallback(
|
||||
(column: string) => {
|
||||
const currentSort = sorting.find((s) => s.id === column)
|
||||
if (currentSort) {
|
||||
if (currentSort.desc) {
|
||||
// Cycle: desc -> remove sort
|
||||
setSorting([])
|
||||
} else {
|
||||
// Cycle: asc -> desc
|
||||
setSorting([{ id: column, desc: true }])
|
||||
}
|
||||
} else {
|
||||
// New column, start with asc
|
||||
setSorting([{ id: column, desc: false }])
|
||||
}
|
||||
},
|
||||
[sorting]
|
||||
)
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
columnVisibility,
|
||||
rowSelection,
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col gap-4">
|
||||
{/* Filters and column visibility controls */}
|
||||
<div className="flex items-center">
|
||||
<Input
|
||||
size="tiny"
|
||||
placeholder="Filter by email"
|
||||
value={(table.getColumn('email')?.getFilterValue() as string) ?? ''}
|
||||
onChange={(event) => table.getColumn('email')?.setFilterValue(event.target.value)}
|
||||
className="max-w-sm"
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="default" className="ml-auto" size="tiny" iconRight={<ChevronDown />}>
|
||||
Columns
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="max-w-48">
|
||||
{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>
|
||||
)
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
{/* Table */}
|
||||
<Card className="w-full">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
const columnId = header.column.id
|
||||
const canSort = header.column.getCanSort()
|
||||
|
||||
return (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
className={
|
||||
columnId === 'amount'
|
||||
? 'text-right'
|
||||
: columnId === 'actions'
|
||||
? 'w-1'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{header.isPlaceholder ? null : canSort ? (
|
||||
<TableHeadSort
|
||||
column={columnId}
|
||||
currentSort={getSortString}
|
||||
onSortChange={handleSortChange}
|
||||
className={columnId === 'amount' ? 'justify-end' : undefined}
|
||||
>
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</TableHeadSort>
|
||||
) : (
|
||||
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}
|
||||
className={
|
||||
cell.column.id === 'email'
|
||||
? 'text-foreground-lighter'
|
||||
: cell.column.id === 'actions'
|
||||
? 'w-1'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length}>
|
||||
<p className="text-sm text-foreground">No results found</p>
|
||||
<p className="text-sm text-foreground-lighter">
|
||||
Your search did not return any results
|
||||
</p>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
{/* Count and pagination controls */}
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<div className="text-foreground-muted flex-1 text-xs">
|
||||
{table.getFilteredSelectedRowModel().rows.length} of{' '}
|
||||
{table.getFilteredRowModel().rows.length} row(s) selected
|
||||
</div>
|
||||
<div className="space-x-2">
|
||||
<Button
|
||||
type="default"
|
||||
size="tiny"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
size="tiny"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1475,9 +1475,20 @@ export const examples: Registry = [
|
||||
files: ['example/empty-state-initial-state-informational.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'empty-state-zero-items-data-grid',
|
||||
name: 'data-grid-demo',
|
||||
type: 'components:example',
|
||||
files: ['example/empty-state-zero-items-data-grid.tsx'],
|
||||
files: ['example/data-grid-demo.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'data-grid-empty-state',
|
||||
type: 'components:example',
|
||||
files: ['example/data-grid-empty-state.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'data-table-demo',
|
||||
type: 'components:example',
|
||||
registryDependencies: ['table'],
|
||||
files: ['example/data-table-demo.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'metric-card',
|
||||
|
||||
Generated
+171
@@ -237,6 +237,9 @@ importers:
|
||||
'@hookform/resolvers':
|
||||
specifier: ^3.1.1
|
||||
version: 3.3.1(react-hook-form@7.47.0(react@18.3.1))
|
||||
'@tanstack/react-table':
|
||||
specifier: ^8.21.3
|
||||
version: 8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
contentlayer2:
|
||||
specifier: 0.4.6
|
||||
version: 0.4.6(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1)
|
||||
@@ -4831,6 +4834,110 @@ packages:
|
||||
'@mdx-js/react':
|
||||
optional: true
|
||||
|
||||
'@next/swc-darwin-arm64@15.5.7':
|
||||
resolution: {integrity: sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-darwin-arm64@16.0.7':
|
||||
resolution: {integrity: sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-darwin-x64@15.5.7':
|
||||
resolution: {integrity: sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-darwin-x64@16.0.7':
|
||||
resolution: {integrity: sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-linux-arm64-gnu@15.5.7':
|
||||
resolution: {integrity: sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-arm64-gnu@16.0.7':
|
||||
resolution: {integrity: sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-arm64-musl@15.5.7':
|
||||
resolution: {integrity: sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-linux-arm64-musl@16.0.7':
|
||||
resolution: {integrity: sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-linux-x64-gnu@15.5.7':
|
||||
resolution: {integrity: sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-x64-gnu@16.0.7':
|
||||
resolution: {integrity: sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-x64-musl@15.5.7':
|
||||
resolution: {integrity: sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-linux-x64-musl@16.0.7':
|
||||
resolution: {integrity: sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@15.5.7':
|
||||
resolution: {integrity: sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@16.0.7':
|
||||
resolution: {integrity: sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@next/swc-win32-x64-msvc@15.5.7':
|
||||
resolution: {integrity: sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@next/swc-win32-x64-msvc@16.0.7':
|
||||
resolution: {integrity: sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@noble/ciphers@1.3.0':
|
||||
resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==}
|
||||
engines: {node: ^14.21.3 || >=16}
|
||||
@@ -22821,6 +22928,54 @@ snapshots:
|
||||
'@mdx-js/loader': 2.3.0(supports-color@8.1.1)(webpack@5.94.0)
|
||||
'@mdx-js/react': 2.3.0(react@18.3.1)
|
||||
|
||||
'@next/swc-darwin-arm64@15.5.7':
|
||||
optional: true
|
||||
|
||||
'@next/swc-darwin-arm64@16.0.7':
|
||||
optional: true
|
||||
|
||||
'@next/swc-darwin-x64@15.5.7':
|
||||
optional: true
|
||||
|
||||
'@next/swc-darwin-x64@16.0.7':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-gnu@15.5.7':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-gnu@16.0.7':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-musl@15.5.7':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-musl@16.0.7':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-gnu@15.5.7':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-gnu@16.0.7':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-musl@15.5.7':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-musl@16.0.7':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-arm64-msvc@15.5.7':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-arm64-msvc@16.0.7':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-x64-msvc@15.5.7':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-x64-msvc@16.0.7':
|
||||
optional: true
|
||||
|
||||
'@noble/ciphers@1.3.0': {}
|
||||
|
||||
'@noble/curves@1.9.7':
|
||||
@@ -35446,6 +35601,14 @@ snapshots:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
styled-jsx: 5.1.6(@babel/core@7.28.4(supports-color@8.1.1))(babel-plugin-macros@3.1.0)(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 15.5.7
|
||||
'@next/swc-darwin-x64': 15.5.7
|
||||
'@next/swc-linux-arm64-gnu': 15.5.7
|
||||
'@next/swc-linux-arm64-musl': 15.5.7
|
||||
'@next/swc-linux-x64-gnu': 15.5.7
|
||||
'@next/swc-linux-x64-musl': 15.5.7
|
||||
'@next/swc-win32-arm64-msvc': 15.5.7
|
||||
'@next/swc-win32-x64-msvc': 15.5.7
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@playwright/test': 1.56.1
|
||||
sass: 1.77.4
|
||||
@@ -35464,6 +35627,14 @@ snapshots:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
styled-jsx: 5.1.6(@babel/core@7.28.4(supports-color@8.1.1))(babel-plugin-macros@3.1.0)(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 16.0.7
|
||||
'@next/swc-darwin-x64': 16.0.7
|
||||
'@next/swc-linux-arm64-gnu': 16.0.7
|
||||
'@next/swc-linux-arm64-musl': 16.0.7
|
||||
'@next/swc-linux-x64-gnu': 16.0.7
|
||||
'@next/swc-linux-x64-musl': 16.0.7
|
||||
'@next/swc-win32-arm64-msvc': 16.0.7
|
||||
'@next/swc-win32-x64-msvc': 16.0.7
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@playwright/test': 1.56.1
|
||||
sass: 1.77.4
|
||||
|
||||
Reference in new issue
Block a user