feat: components 패키지에 Table, DataTable 컴포넌트 추가

This commit is contained in:
2025-02-27 18:39:18 +09:00
parent 87a4866a5b
commit d77f46d121
15 changed files with 416 additions and 2104 deletions
+2 -1
View File
@@ -13,7 +13,8 @@
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
}
},
"./styles/*": "./dist/styles/*"
},
"scripts": {
"build": "tsup",
@@ -1,5 +1,4 @@
import "../src/base.css";
import "../src/diveicon.css";
import React from "react";
import type { Preview } from "@storybook/react";
+1 -3
View File
@@ -12,9 +12,7 @@
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
},
"./base.css": "./src/base.css",
"./diveicon.css": "./src/diveicon.css"
}
},
"files": [
"dist/**"
+3 -58
View File
@@ -1,65 +1,10 @@
@import "@common/assets/styles/base.css";
@import "@common/assets/styles/diveicon.css";
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: #ffffff;
--foreground: #0a0a0a;
--card: #ffffff;
--card-foreground: #0a0a0a;
--popover: #ffffff;
--popover-foreground: #0a0a0a;
--primary: #2094fa;
--primary-foreground: #fafafa;
--secondary: #f5f5f5;
--secondary-foreground: #0f1e33;
--muted: #f5f5f5;
--muted-foreground: #737373;
--accent: #f5f5f5;
--accent-foreground: #0f1e33;
--destructive: #ff4141;
--destructive-foreground: #fafafa;
--border: #e5e5e5;
--input: #e5e5e5;
--label: #666666;
--ring: #0a0a0a;
--chart-1: #d95b43;
--chart-2: #299392;
--chart-3: #263e4d;
--chart-4: #dfb938;
--chart-5: #f08634;
--radius: 1rem;
}
[data-mode="dark"] {
--background: #0a0a0a;
--foreground: #fafafa;
--card: #0a0a0a;
--card-foreground: #fafafa;
--popover: #0a0a0a;
--popover-foreground: #fafafa;
--primary: #2094fa;
--primary-foreground: #171717;
--secondary: #262626;
--secondary-foreground: #fafafa;
--muted: #262626;
--muted-foreground: #a2a2a2;
--accent: #262626;
--accent-foreground: #fafafa;
--destructive: #b22222;
--destructive-foreground: #fafafa;
--border: #262626;
--input: #262626;
--label: #f1f1f1;
--ring: #d4d4d4;
--chart-1: #3399ff;
--chart-2: #339966;
--chart-3: #ff9933;
--chart-4: #cc66ff;
--chart-5: #ff3399;
}
}
@layer base {
* {
@apply border-border;
File diff suppressed because it is too large Load Diff
@@ -22,7 +22,7 @@ const buttonVariants = cva(
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-12 px-10",
default: "h-[3.2rem] px-10",
xs: "h-8 px-2 !text-[1.1rem]",
sm: "h-10 px-4",
lg: "h-16 px-12",
@@ -0,0 +1,157 @@
import type { Meta, StoryObj } from "@storybook/react";
import { createColumnHelper } from "@tanstack/react-table";
import { DataTable } from ".";
const meta: Meta<typeof DataTable> = {
title: "Elements/DataTable",
component: DataTable,
parameters: {
docs: {
description: {
component: `
- [Docs](https://tanstack.com/table/v8/docs/introduction)
`,
},
source: {
code: `
import { createColumnHelper } from "@tanstack/react-table";
type Person = {
firstName: string;
lastName: string;
age: number;
visits: number;
status: string;
progress: number;
};
const defaultData: Person[] = [
{
firstName: "tanner",
lastName: "linsley",
age: 24,
visits: 100,
status: "In Relationship",
progress: 50,
},
{
firstName: "tandy",
lastName: "miller",
age: 40,
visits: 40,
status: "Single",
progress: 80,
},
{
firstName: "joe",
lastName: "dirte",
age: 45,
visits: 20,
status: "Complicated",
progress: 10,
},
];
const columnHelper = createColumnHelper<Person>();
const columns = [
columnHelper.accessor("firstName", {
cell: (info) => info.getValue(),
}),
columnHelper.accessor((row) => row.lastName, {
id: "lastName",
cell: (info) => <i>{info.getValue()}</i>,
header: () => <span>Last Name</span>,
}),
columnHelper.accessor("age", {
header: () => "Age",
cell: (info) => info.renderValue(),
}),
columnHelper.accessor("visits", {
header: () => <span>Visits</span>,
}),
columnHelper.accessor("status", {
header: "Status",
}),
columnHelper.accessor("progress", {
header: "Profile Progress",
}),
];
return <Table data={defaultData} columns={columns} />;
`,
},
},
},
};
export default meta;
type Story = StoryObj<typeof DataTable>;
export const Default: Story = {
render: () => {
type Person = {
firstName: string;
lastName: string;
age: number;
visits: number;
status: string;
progress: number;
};
const defaultData: Person[] = [
{
firstName: "tanner",
lastName: "linsley",
age: 24,
visits: 100,
status: "In Relationship",
progress: 50,
},
{
firstName: "tandy",
lastName: "miller",
age: 40,
visits: 40,
status: "Single",
progress: 80,
},
{
firstName: "joe",
lastName: "dirte",
age: 45,
visits: 20,
status: "Complicated",
progress: 10,
},
];
const columnHelper = createColumnHelper<Person>();
const columns = [
columnHelper.accessor("firstName", {
cell: (info) => info.getValue(),
}),
columnHelper.accessor((row) => row.lastName, {
id: "lastName",
cell: (info) => <i>{info.getValue()}</i>,
header: () => <span>Last Name</span>,
}),
columnHelper.accessor("age", {
header: () => "Age",
cell: (info) => info.renderValue(),
}),
columnHelper.accessor("visits", {
header: () => <span>Visits</span>,
}),
columnHelper.accessor("status", {
header: "Status",
}),
columnHelper.accessor("progress", {
header: "Profile Progress",
}),
];
return <DataTable data={defaultData} columns={columns} />;
},
};
@@ -0,0 +1,76 @@
import {
flexRender,
getCoreRowModel,
useReactTable,
type ColumnDef,
type RowData,
type Table as TableType,
} from "@tanstack/react-table";
import { Table, TableCell, TableHead, TableHeader, TableRow } from "../table";
declare module "@tanstack/react-table" {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface ColumnMeta<TData extends RowData, TValue> {
align: "left" | "center" | "right";
}
}
function THead<T>({ table }: { table: TableType<T> }) {
return (
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
const { meta } = header.column.columnDef;
return (
<TableHead key={header.id} className="sticky top-0" {...meta}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
);
}
interface TableProps<T> {
data: T[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
columns: ColumnDef<T, any>[];
}
function DataTable<T>({ data, columns }: TableProps<T>) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
});
return (
<Table className="w-full">
<THead table={table} />
<tbody>
{table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => {
const { meta } = cell.column.columnDef;
return (
<TableCell key={cell.id} align="center" {...meta}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
);
})}
</TableRow>
))}
</tbody>
</Table>
);
}
export { DataTable };
@@ -3,6 +3,7 @@ export * from "./badge";
export * from "./button";
export * from "./calendar";
export * from "./checkbox";
export * from "./data-table";
export * from "./date-picker";
export * from "./dropdown-menu";
export * from "./input";
@@ -8,7 +8,7 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"flex h-[3.2rem] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
ref={ref}
+111 -82
View File
@@ -1,90 +1,119 @@
import "./table.css";
import {
flexRender,
getCoreRowModel,
useReactTable,
type ColumnDef,
type RowData,
type Table,
} from "@tanstack/react-table";
import * as React from "react";
declare module "@tanstack/react-table" {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface ColumnMeta<TData extends RowData, TValue> {
align: "left" | "center" | "right";
}
}
import { cn } from "../../lib/utils";
function TableHead<T>({ table }: { table: Table<T> }) {
return (
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th key={header.id} className="sticky top-0">
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</th>
))}
</tr>
))}
</thead>
);
}
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
));
Table.displayName = "Table";
interface TableProps<T> {
data: T[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
columns: ColumnDef<T, any>[];
}
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
));
TableHeader.displayName = "TableHeader";
function BasicTable<T>({ data, columns }: TableProps<T>) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
});
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
));
TableBody.displayName = "TableBody";
return (
<table className="w-full">
<TableHead table={table} />
<tbody>
{table.getRowModel().rows.map((row) => (
<tr key={row.id}>
{row.getVisibleCells().map((cell) => {
const { meta } = cell.column.columnDef;
return (
<td key={cell.id} align="center" {...meta}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
);
})}
</tr>
))}
</tbody>
{/* <tfoot>
{table.getFooterGroups().map((footerGroup) => (
<tr key={footerGroup.id}>
{footerGroup.headers.map((header) => (
<th key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.footer,
header.getContext(),
)}
</th>
))}
</tr>
))}
</tfoot> */}
</table>
);
}
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
className,
)}
{...props}
/>
));
TableFooter.displayName = "TableFooter";
export { BasicTable };
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"hover:bg-muted/50 border-b transition-colors data-[state=selected]:bg-muted",
className,
)}
{...props}
/>
));
TableRow.displayName = "TableRow";
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className,
)}
{...props}
/>
));
TableHead.displayName = "TableHead";
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
));
TableCell.displayName = "TableCell";
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
));
TableCaption.displayName = "TableCaption";
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
};
@@ -1,95 +1,17 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Table } from ".";
import { createColumnHelper } from "@tanstack/react-table";
import {
Table,
TableHeader,
TableBody,
TableHead,
TableRow,
TableCell,
} from "./index";
const meta: Meta<typeof Table> = {
title: "Elements/Table",
component: Table,
parameters: {
docs: {
description: {
component: `
- [Docs](https://ui.shadcn.com/docs/components/switch)
- [API Reference](https://www.radix-ui.com/primitives/docs/components/switch#api-reference)
`,
},
source: {
code: `
import { createColumnHelper } from "@tanstack/react-table";
type Person = {
firstName: string;
lastName: string;
age: number;
visits: number;
status: string;
progress: number;
};
const defaultData: Person[] = [
{
firstName: "tanner",
lastName: "linsley",
age: 24,
visits: 100,
status: "In Relationship",
progress: 50,
},
{
firstName: "tandy",
lastName: "miller",
age: 40,
visits: 40,
status: "Single",
progress: 80,
},
{
firstName: "joe",
lastName: "dirte",
age: 45,
visits: 20,
status: "Complicated",
progress: 10,
},
];
const columnHelper = createColumnHelper<Person>();
const columns = [
columnHelper.accessor("firstName", {
cell: (info) => info.getValue(),
footer: (info) => info.column.id,
}),
columnHelper.accessor((row) => row.lastName, {
id: "lastName",
cell: (info) => <i>{info.getValue()}</i>,
header: () => <span>Last Name</span>,
footer: (info) => info.column.id,
}),
columnHelper.accessor("age", {
header: () => "Age",
cell: (info) => info.renderValue(),
footer: (info) => info.column.id,
}),
columnHelper.accessor("visits", {
header: () => <span>Visits</span>,
footer: (info) => info.column.id,
}),
columnHelper.accessor("status", {
header: "Status",
footer: (info) => info.column.id,
}),
columnHelper.accessor("progress", {
header: "Profile Progress",
footer: (info) => info.column.id,
}),
];
return <Table data={defaultData} columns={columns} />;
`,
},
},
},
tags: ["autodocs"],
};
export default meta;
@@ -97,74 +19,60 @@ type Story = StoryObj<typeof Table>;
export const Default: Story = {
render: () => {
type Person = {
firstName: string;
lastName: string;
age: number;
visits: number;
status: string;
progress: number;
};
const defaultData: Person[] = [
const invoices = [
{
firstName: "tanner",
lastName: "linsley",
age: 24,
visits: 100,
status: "In Relationship",
progress: 50,
invoice: "INV001",
paymentStatus: "Paid",
totalAmount: "$250.00",
paymentMethod: "Credit Card",
},
{
firstName: "tandy",
lastName: "miller",
age: 40,
visits: 40,
status: "Single",
progress: 80,
invoice: "INV002",
paymentStatus: "Pending",
totalAmount: "$150.00",
paymentMethod: "PayPal",
},
{
firstName: "joe",
lastName: "dirte",
age: 45,
visits: 20,
status: "Complicated",
progress: 10,
invoice: "INV003",
paymentStatus: "Unpaid",
totalAmount: "$350.00",
paymentMethod: "Bank Transfer",
},
{
invoice: "INV004",
paymentStatus: "Paid",
totalAmount: "$450.00",
paymentMethod: "Credit Card",
},
{
invoice: "INV005",
paymentStatus: "Paid",
totalAmount: "$550.00",
paymentMethod: "PayPal",
},
];
const columnHelper = createColumnHelper<Person>();
const columns = [
columnHelper.accessor("firstName", {
cell: (info) => info.getValue(),
footer: (info) => info.column.id,
}),
columnHelper.accessor((row) => row.lastName, {
id: "lastName",
cell: (info) => <i>{info.getValue()}</i>,
header: () => <span>Last Name</span>,
footer: (info) => info.column.id,
}),
columnHelper.accessor("age", {
header: () => "Age",
cell: (info) => info.renderValue(),
footer: (info) => info.column.id,
}),
columnHelper.accessor("visits", {
header: () => <span>Visits</span>,
footer: (info) => info.column.id,
}),
columnHelper.accessor("status", {
header: "Status",
footer: (info) => info.column.id,
}),
columnHelper.accessor("progress", {
header: "Profile Progress",
footer: (info) => info.column.id,
}),
];
return <Table data={defaultData} columns={columns} />;
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Amount</TableHead>
<TableHead>Payment Method</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{invoices.map((invoice) => (
<TableRow key={invoice.invoice}>
<TableCell className="font-medium">{invoice.invoice}</TableCell>
<TableCell>{invoice.paymentStatus}</TableCell>
<TableCell>{invoice.totalAmount}</TableCell>
<TableCell>{invoice.paymentMethod}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
},
};
+1 -3
View File
@@ -14,9 +14,7 @@
"strict": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@common/assets/*": ["../assets/dist/*"],
"@common/assets": ["../assets/dist"]
"@/*": ["src/*"]
}
},
"exclude": ["**/node_modules", "**/.*/*", "dist"],
-4
View File
@@ -1,4 +0,0 @@
declare module "*.mdx" {
let MDXComponent: (props: unknown) => JSX.Element;
export default MDXComponent;
}
+6 -3
View File
@@ -33,6 +33,9 @@ importers:
apps/cs:
dependencies:
'@common/assets':
specifier: workspace:*
version: link:../../packages/common/assets
'@common/components':
specifier: workspace:*
version: link:../../packages/common/components
@@ -3328,7 +3331,7 @@ packages:
hasBin: true
dependencies:
caniuse-lite: 1.0.30001701
electron-to-chromium: 1.5.106
electron-to-chromium: 1.5.107
node-releases: 2.0.19
update-browserslist-db: 1.1.3(browserslist@4.24.4)
dev: true
@@ -3797,8 +3800,8 @@ packages:
engines: {node: '>=11.0.0'}
dev: true
/electron-to-chromium@1.5.106:
resolution: {integrity: sha512-12keJGdXQWPnfVOu6/6ZzZgPPNodiDOSe3LjA8qk2yXTjnCnw2LeGUsAmtlNAmH4UW0K7tOLcz0j9lI2eJCJRA==}
/electron-to-chromium@1.5.107:
resolution: {integrity: sha512-dJr1o6yCntRkXElnhsHh1bAV19bo/hKyFf7tCcWgpXbuFIF0Lakjgqv5LRfSDaNzAII8Fnxg2tqgHkgCvxdbxw==}
dev: true
/emoji-regex@8.0.0: