New Changes with Table Component

This commit is contained in:
2025-10-27 12:55:37 +05:30
parent 7c5274e2f1
commit b09a06e875
7 changed files with 838 additions and 2 deletions
+1 -1
View File
@@ -90,7 +90,7 @@ const iconSize =
const labelCls =
"wr:block wr:text-sm wr:font-medium wr:mb-2 wr:dark:text-white";
const helpCls = "wr:mt-2 wr:text-sm wr:text-gray-500 wr:dark:text-neutral-500";
const errCls = "wr:text-sm wr:text-red-600 wr:mt-2";
const errCls = "wr:text-sm wr:text-red-600 wr:mt-2 wr:capitalize";
const groupCls = "wr:relative wr:flex wr:items-stretch";
const addonBase =
"wr:px-4 wr:inline-flex wr:items-center wr:min-w-fit wr:rounded-md wr:border wr:border-border wr:bg-gray-200 wr:text-sm wr:text-gray-500 wr:dark:bg-card wr:dark:border-border wr:dark:text-neutral-400";
+410
View File
@@ -0,0 +1,410 @@
---
import type { TableProps } from "../types/table/types";
const {
columns = [],
data = [],
selectable = false,
search = false,
striped = false,
pageSize = 10,
fetchData,
columnsRendererKey,
} = Astro.props as TableProps<any>;
const id = `table-${Math.random().toString(36).slice(2, 9)}`;
---
<div id={id} class="wr:space-y-3">
{
search && (
<div class="wr:flex wr:justify-between wr:items-center wr:gap-2">
<input
type="text"
placeholder="Search..."
class="wr:w-full wr:rounded-md wr:border wr:border-gray-300 wr:dark:border-neutral-700 wr:bg-white wr:dark:bg-neutral-800 wr:px-3 wr:py-2 wr:text-sm wr:focus:ring-2 wr:focus:ring-primary"
data-search
/>
</div>
)
}
<!-- 🖥️ Desktop Table -->
<div
class="wr:overflow-x-auto wr:rounded-xl wr:border wr:dark:border-neutral-700 wr:hidden wr:md:block"
>
<table
class={`wr:w-full wr:text-sm ${striped ? "wr:divide-y wr:divide-gray-200 wr:dark:divide-neutral-700" : ""}`}
>
<thead
class="wr:bg-gray-100 wr:dark:bg-neutral-700 wr:text-gray-600 wr:dark:text-gray-300"
>
<tr>
{
selectable && (
<th class="wr:p-3 wr:text-left wr:w-12">
<input type="checkbox" data-select-all />
</th>
)
}
{
columns.map((col) => (
<th
class={`wr:py-3 wr:px-4 ${col.align ? `wr:text-${col.align}` : "wr:text-left"} ${col.sortable ? "wr:cursor-pointer" : ""}`}
style={col.width ? `width:${col.width}` : ""}
data-sort-key={
col.sortable ? col.key : undefined
}
>
<div class="wr:flex wr:items-center wr:gap-1">
<span>{col.label}</span>
{col.sortable && (
<iconify-icon
icon="solar:sort-vertical-bold-duotone"
class="wr:text-lg wr:text-gray-400 sort-indicator"
/>
)}
</div>
</th>
))
}
</tr>
</thead>
<tbody data-body>
<!-- {
data.map((row, i) => (
<tr
class={`wr:transition wr:bg-white wr:dark:bg-neutral-900 wr:hover:bg-gray-100 wr:dark:hover:bg-neutral-700 ${striped ? "wr:border-b wr:dark:border-neutral-800" : ""}`}
>
{selectable && (
<td class="wr:px-4 wr:py-3">
<input
type="checkbox"
data-row-select
data-index={i}
/>
</td>
)}
{columns.map((col) => (
<td
class={`wr:px-4 wr:py-3 ${col.align ? `wr:text-${col.align}` : "wr:text-left"} wr:whitespace-nowrap`}
data-key={col.key}
>
{col.render ? (
<Fragment
set:html={col.render(
row[col.key],
row,
)}
/>
) : (
String(row[col.key] ?? "")
)}
</td>
))}
</tr>
))
} -->
</tbody>
</table>
</div>
<!-- 📱 Mobile Cards -->
<div class="wr:space-y-4 wr:md:hidden" data-mobile-body>
<!-- {
data.map((row, i) => (
<div class="wr:rounded-lg wr:border wr:border-gray-200 wr:dark:border-neutral-700 wr:p-4 wr:space-y-2 wr:bg-white wr:dark:bg-neutral-800">
{columns.map((col) => (
<div class="wr:flex wr:justify-between wr:items-start wr:gap-4">
<div class="wr:text-sm wr:font-medium wr:text-gray-600 wr:dark:text-gray-300">
{col.label}
</div>
<div class="wr:text-sm wr:text-gray-800 wr:dark:text-gray-100 wr:text-right wr:max-w-[60%] wr:break-words">
{col.render ? (
<Fragment
set:html={col.render(row[col.key], row)}
/>
) : (
String(row[col.key] ?? "")
)}
</div>
</div>
))}
</div>
))
} -->
</div>
<!-- 📄 Pagination -->
<div
class="wr:flex wr:justify-between wr:items-center wr:gap-4 wr:flex-wrap wr:mt-4"
>
<div class="wr:flex wr:items-center wr:gap-2 wr:text-sm">
<span class="wr:text-gray-600 wr:dark:text-gray-400"
>Rows per page:</span
>
<select
class="wr:w-[75px] wr:rounded-md wr:border wr:border-gray-300 wr:dark:border-neutral-600 wr:bg-white wr:dark:bg-neutral-800 wr:px-2 wr:py-1 wr:text-sm wr:focus:ring-2 wr:focus:ring-primary"
data-page-size
>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
<div class="wr:flex wr:items-center wr:gap-3 wr:flex-wrap">
<div
class="wr:text-sm wr:text-gray-600 wr:dark:text-gray-400"
data-page-info
>
</div>
<div
class="wr:flex wr:items-center wr:gap-1 wr:bg-gray-50 wr:dark:bg-neutral-800 wr:rounded-full wr:px-2 wr:py-1 wr:shadow-sm"
data-pagination
>
</div>
</div>
</div>
</div>
<script
type="module"
is:inline
define:vars={{ id, pageSize, fetchData, columns, data, columnsRendererKey }}
>
const start = () => {
const table = document.getElementById(id);
const searchInput = table.querySelector("[data-search]");
const sortHeaders = table.querySelectorAll("[data-sort-key]");
const tbody = table.querySelector("[data-body]");
const mobileBody = table.querySelector("[data-mobile-body]");
const pageInfo = table.querySelector("[data-page-info]");
const paginationContainer = table.querySelector("[data-pagination]");
const pageSizeSelector = table.querySelector("[data-page-size]");
let currentPage = 1;
let rowsPerPage = parseInt(pageSizeSelector.value);
let sortKey = null;
let sortOrder = "asc";
let totalRows = data.length;
let currentData = [...data];
const arrowLeftSVG = `
<svg xmlns="http://www.w3.org/2000/svg" class="wr:w-5 wr:h-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" />
</svg>`;
const arrowRightSVG = `
<svg xmlns="http://www.w3.org/2000/svg" class="wr:w-5 wr:h-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
</svg>`;
const fetchDataFn = fetchData ? window[fetchData] : null;
/** 🧱 Render rows */
function renderRows(dataset) {
tbody.innerHTML = "";
mobileBody.innerHTML = "";
dataset.forEach((row) => {
// 🖥️ Desktop
let tr = `<tr class="wr:transition wr:bg-white wr:dark:bg-neutral-900 wr:hover:bg-gray-100 wr:dark:hover:bg-neutral-700 wr:border-b wr:dark:border-neutral-800">`;
columns.forEach((col) => {
let value = row[col.key] ?? "";
const renderers = columnsRendererKey
? window[columnsRendererKey]
: {};
columns.forEach((col) => {
if (renderers[col.key]) col.render = renderers[col.key];
});
if (col.render) {
value = col.render(value, row);
}
tr += `<td class="wr:px-4 wr:py-3">${value}</td>`;
});
tr += `</tr>`;
tbody.insertAdjacentHTML("beforeend", tr);
// 📱 Mobile
let card = `<div class="wr:rounded-lg wr:border wr:border-gray-200 wr:dark:border-neutral-700 wr:p-4 wr:space-y-2 wr:bg-white wr:dark:bg-neutral-800">`;
columns.forEach((col) => {
let value = row[col.key] ?? "";
const renderers = columnsRendererKey
? window[columnsRendererKey]
: {};
columns.forEach((col) => {
if (renderers[col.key]) col.render = renderers[col.key];
});
if (col.render) {
value = col.render(value, row);
}
card += `
<div class="wr:flex wr:justify-between wr:items-start wr:gap-4">
<div class="wr:text-sm wr:font-medium wr:text-gray-600 wr:dark:text-gray-300">${col.label}</div>
<div class="wr:text-sm wr:text-gray-800 wr:dark:text-gray-100 wr:text-right wr:max-w-[60%] wr:break-words">${value}</div>
</div>`;
});
card += `</div>`;
mobileBody.insertAdjacentHTML("beforeend", card);
});
}
async function loadData() {
let paginated = [];
if (fetchDataFn) {
const result = await fetchDataFn({
page: currentPage,
pageSize: rowsPerPage,
sortKey,
sortOrder,
search: searchInput?.value || "",
});
currentData = result.data;
totalRows = result.total;
paginated = currentData;
} else {
currentData = [...data];
totalRows = currentData.length;
const start = (currentPage - 1) * rowsPerPage;
const end = start + rowsPerPage;
paginated = currentData.slice(start, end);
}
// sort locally if needed
if (sortKey !== null) {
const colKey = columns[sortKey]?.key;
if (colKey) {
currentData.sort((a, b) => {
const aVal = String(a[colKey] ?? "");
const bVal = String(b[colKey] ?? "");
if (aVal < bVal) return sortOrder === "asc" ? -1 : 1;
if (aVal > bVal) return sortOrder === "asc" ? 1 : -1;
return 0;
});
}
}
// paginate
renderRows(paginated);
renderPagination(totalRows);
}
/** 📄 Pagination */
function renderPagination(totalRows) {
const totalPages = Math.max(1, Math.ceil(totalRows / rowsPerPage));
paginationContainer.innerHTML = "";
const createButton = (
label,
page,
disabled = false,
isActive = false,
icon = null,
) => {
const btn = document.createElement("button");
btn.className = `
wr:flex wr:items-center wr:justify-center wr:w-8 wr:h-8
wr:rounded-full wr:text-sm wr:transition
${
isActive
? "wr:bg-primary wr:text-white wr:shadow-md"
: "wr:text-gray-600 wr:dark:text-gray-300 wr:hover:bg-gray-200 wr:dark:hover:bg-neutral-700"
}
${disabled ? "wr:opacity-50 wr:cursor-not-allowed" : ""}
`;
if (icon === "left") btn.innerHTML = arrowLeftSVG;
else if (icon === "right") btn.innerHTML = arrowRightSVG;
else btn.textContent = label;
if (!disabled && !isActive) {
btn.addEventListener("click", () => {
currentPage = page;
loadData();
});
}
paginationContainer.appendChild(btn);
};
createButton("", currentPage - 1, currentPage === 1, false, "left");
const maxVisible = 5;
let start = Math.max(1, currentPage - Math.floor(maxVisible / 2));
let end = Math.min(totalPages, start + maxVisible - 1);
if (end - start < maxVisible - 1)
start = Math.max(1, end - maxVisible + 1);
for (let i = start; i <= end; i++)
createButton(i, i, false, i === currentPage);
createButton(
"",
currentPage + 1,
currentPage === totalPages,
false,
"right",
);
pageInfo.textContent = `Page ${currentPage} of ${totalPages} • ${totalRows} total`;
}
// 🕵️ Search
searchInput?.addEventListener("input", () => {
currentPage = 1;
loadData();
});
// 🧭 Sorting
sortHeaders.forEach((th, index) => {
th.addEventListener("click", () => {
if (sortKey === index)
sortOrder = sortOrder === "asc" ? "desc" : "asc";
else {
sortKey = index;
sortOrder = "asc";
}
sortHeaders.forEach((h) =>
h
.querySelector(".sort-indicator")
?.setAttribute(
"icon",
"solar:sort-vertical-bold-duotone",
),
);
th.querySelector(".sort-indicator")?.setAttribute(
"icon",
sortOrder === "asc"
? "solar:sort-up-bold-duotone"
: "solar:sort-down-bold-duotone",
);
loadData();
});
});
// 📊 Page size
pageSizeSelector?.addEventListener("change", (e) => {
rowsPerPage = parseInt(e.target.value);
currentPage = 1;
loadData();
});
// 🚀 Initial
loadData();
};
document.addEventListener("astro:page-load", start);
document.addEventListener("DOMContentLoaded", start);
</script>
+2
View File
@@ -6,6 +6,8 @@ declare global {
schema: any;
submit: any;
upload: any;
fetchData: any;
columnsRenderer: any
myModal_open: any;
myModal_close: any;
hls: typeof Hls,
+3 -1
View File
@@ -17,5 +17,7 @@ export { default as Panel } from "./components/Panel.astro";
export { default as PasswordField } from "./components/PasswordField.astro";
export { default as ThemeSwitcher } from "./components/ThemeSwitcher.astro";
export { default as TimePicker } from "./components/TimePicker.astro";
export { default as Table } from "./components/Table.astro";
export { default as Tooltip } from "./components/Tooltip.astro";
export { cn } from "./utils/cn.js";
export { cn } from "./utils/cn.js";
export type { Column, TableProps } from "./types/table/types.js";
+3
View File
@@ -83,6 +83,9 @@ import Base from "./Base.astro";
<li class="list-item">
<a href="/webrtc-player">WebRTC Player</a>
</li>
<li class="list-item">
<a href="/table">Table</a>
</li>
</ul>
</div>
+399
View File
@@ -0,0 +1,399 @@
---
import ComponentLayout from "../layouts/ComponentLayout.astro";
import Table from "../components/Table.astro";
export const columns = [
{ key: "id", label: "ID", sortable: true, searchable: true },
{ key: "name", label: "Name", searchable: true, sortable: true },
{ key: "email", label: "Email", searchable: true, sortable: true },
{ key: "role", label: "Role", sortable: true },
{ key: "status", label: "Status", sortable: true },
{ key: "created_at", label: "Created At", sortable: true },
{ key: "actions", label: "Actions" },
];
const data = [
{
id: 1,
name: "Ajay Ghanwat",
email: "ajay.gh@example.com",
role: "Admin",
status: "Active",
created_at: "2025-10-10 09:32",
avatar: "https://i.pravatar.cc/40?img=1",
},
{
id: 2,
name: "Rahul Sharma",
email: "rahul.sharma@example.com",
role: "Member",
status: "Pending",
created_at: "2025-10-15 11:42",
avatar: "https://i.pravatar.cc/40?img=2",
},
{
id: 3,
name: "Sophia Patel",
email: "sophia.patel@example.com",
role: "Manager",
status: "Active",
created_at: "2025-10-11 15:20",
avatar: "https://i.pravatar.cc/40?img=3",
},
{
id: 4,
name: "Liam Brown",
email: "liam.brown@example.com",
role: "Member",
status: "Suspended",
created_at: "2025-10-12 08:45",
avatar: "https://i.pravatar.cc/40?img=4",
},
{
id: 5,
name: "Olivia Smith",
email: "olivia.smith@example.com",
role: "Admin",
status: "Active",
created_at: "2025-10-09 18:12",
avatar: "https://i.pravatar.cc/40?img=5",
},
{
id: 6,
name: "Noah Wilson",
email: "noah.wilson@example.com",
role: "Member",
status: "Pending",
created_at: "2025-10-13 10:18",
avatar: "https://i.pravatar.cc/40?img=6",
},
{
id: 7,
name: "Emma Johnson",
email: "emma.johnson@example.com",
role: "Member",
status: "Active",
created_at: "2025-10-14 13:55",
avatar: "https://i.pravatar.cc/40?img=7",
},
{
id: 8,
name: "William Lee",
email: "william.lee@example.com",
role: "Manager",
status: "Inactive",
created_at: "2025-10-12 17:44",
avatar: "https://i.pravatar.cc/40?img=8",
},
{
id: 9,
name: "Isabella Taylor",
email: "isabella.taylor@example.com",
role: "Member",
status: "Active",
created_at: "2025-10-10 07:31",
avatar: "https://i.pravatar.cc/40?img=9",
},
{
id: 10,
name: "Ethan Martinez",
email: "ethan.martinez@example.com",
role: "Admin",
status: "Suspended",
created_at: "2025-10-08 19:22",
avatar: "https://i.pravatar.cc/40?img=10",
},
{
id: 11,
name: "Mia Thompson",
email: "mia.thompson@example.com",
role: "Member",
status: "Active",
created_at: "2025-10-09 09:45",
avatar: "https://i.pravatar.cc/40?img=11",
},
{
id: 12,
name: "Alexander Davis",
email: "alex.davis@example.com",
role: "Manager",
status: "Pending",
created_at: "2025-10-15 14:10",
avatar: "https://i.pravatar.cc/40?img=12",
},
{
id: 13,
name: "Charlotte Moore",
email: "charlotte.moore@example.com",
role: "Member",
status: "Active",
created_at: "2025-10-14 10:01",
avatar: "https://i.pravatar.cc/40?img=13",
},
{
id: 14,
name: "Benjamin Anderson",
email: "benjamin.anderson@example.com",
role: "Member",
status: "Suspended",
created_at: "2025-10-08 11:32",
avatar: "https://i.pravatar.cc/40?img=14",
},
{
id: 15,
name: "Amelia Harris",
email: "amelia.harris@example.com",
role: "Manager",
status: "Active",
created_at: "2025-10-13 08:50",
avatar: "https://i.pravatar.cc/40?img=15",
},
{
id: 16,
name: "Lucas White",
email: "lucas.white@example.com",
role: "Member",
status: "Active",
created_at: "2025-10-10 17:10",
avatar: "https://i.pravatar.cc/40?img=16",
},
{
id: 17,
name: "Harper Lewis",
email: "harper.lewis@example.com",
role: "Member",
status: "Pending",
created_at: "2025-10-14 09:00",
avatar: "https://i.pravatar.cc/40?img=17",
},
{
id: 18,
name: "Evelyn Hall",
email: "evelyn.hall@example.com",
role: "Manager",
status: "Active",
created_at: "2025-10-11 16:42",
avatar: "https://i.pravatar.cc/40?img=18",
},
{
id: 19,
name: "Henry Allen",
email: "henry.allen@example.com",
role: "Member",
status: "Inactive",
created_at: "2025-10-09 13:25",
avatar: "https://i.pravatar.cc/40?img=19",
},
{
id: 20,
name: "Ava Scott",
email: "ava.scott@example.com",
role: "Admin",
status: "Active",
created_at: "2025-10-15 15:33",
avatar: "https://i.pravatar.cc/40?img=20",
},
{
id: 21,
name: "Michael Young",
email: "michael.young@example.com",
role: "Member",
status: "Pending",
created_at: "2025-10-13 10:20",
avatar: "https://i.pravatar.cc/40?img=21",
},
{
id: 22,
name: "Ella King",
email: "ella.king@example.com",
role: "Manager",
status: "Active",
created_at: "2025-10-14 08:45",
avatar: "https://i.pravatar.cc/40?img=22",
},
{
id: 23,
name: "Daniel Clark",
email: "daniel.clark@example.com",
role: "Member",
status: "Active",
created_at: "2025-10-12 12:05",
avatar: "https://i.pravatar.cc/40?img=23",
},
{
id: 24,
name: "Luna Wright",
email: "luna.wright@example.com",
role: "Member",
status: "Pending",
created_at: "2025-10-15 11:18",
avatar: "https://i.pravatar.cc/40?img=24",
},
{
id: 25,
name: "Jack Turner",
email: "jack.turner@example.com",
role: "Admin",
status: "Active",
created_at: "2025-10-13 09:50",
avatar: "https://i.pravatar.cc/40?img=25",
},
];
---
<ComponentLayout>
<div class="wr:p-4">
<Table
columns={columns}
data={data}
columnsRendererKey="columnsRenderer"
/>
<br />
<Table
columns={columns}
fetchData="fetchData"
columnsRendererKey="columnsRenderer"
/>
</div>
</ComponentLayout>
<script>
document.addEventListener("DOMContentLoaded", () => {
const buttons = document.querySelectorAll(
"button[id^='view-'], button[id^='edit-'], button[id^='delete-']",
);
buttons.forEach((button) => {
const id = button.getAttribute("data-id");
if (button.id.startsWith("view-")) {
button.addEventListener("click", () => {
console.log(`👁️ View user ${id}`);
});
}
if (button.id.startsWith("edit-")) {
button.addEventListener("click", () => {
console.log(`✏️ Edit user ${id}`);
});
}
if (button.id.startsWith("delete-")) {
button.addEventListener("click", () => {
if (
confirm(`Are you sure you want to delete user ${id}?`)
) {
console.log(`🗑️ Deleted user ${id}`);
}
});
}
});
});
window.columnsRenderer = {
name: (v: any, row: any) => `
<div class="wr:flex wr:items-center wr:gap-2">
<img src="${row.avatar}" alt="${v}" class="wr:w-8 wr:h-8 wr:rounded-full" />
<span class="wr:font-medium">${v}</span>
</div>
`,
role: (v: string) => `
<span class="wr:rounded-full wr:px-2 wr:py-1 wr:text-xs ${
v === "Admin"
? "wr:bg-indigo-100 wr:text-indigo-700"
: v === "Owner"
? "wr:bg-yellow-100 wr:text-yellow-700"
: "wr:bg-gray-100 wr:text-gray-600"
}">${v}</span>
`,
status: (v: string) => `
<span class="wr:flex wr:items-center wr:gap-1 wr:font-medium">
<span class="wr:w-2 wr:h-2 wr:rounded-full ${
v === "Active"
? "wr:bg-green-500"
: v === "Inactive"
? "wr:bg-red-500"
: "wr:bg-yellow-500"
}"></span>
${v}
</span>
`,
actions: (v: any, row: any) => `
<div class="wr:flex wr:gap-2 wr:justify-end">
<button id="view-${row.id}" data-id="${row.id}" class="wr:px-2 wr:py-1 wr:bg-blue-500 wr:text-white wr:rounded wr:hover:bg-blue-600">View</button>
<button id="edit-${row.id}" data-id="${row.id}" class="wr:px-2 wr:py-1 wr:bg-amber-500 wr:text-white wr:rounded wr:hover:bg-amber-600">Edit</button>
<button id="delete-${row.id}" data-id="${row.id}" class="wr:px-2 wr:py-1 wr:bg-red-500 wr:text-white wr:rounded wr:hover:bg-red-600">Delete</button>
</div>
`,
};
window.fetchData = ({
page,
pageSize,
sortKey,
sortOrder,
search,
}: {
page: number;
pageSize: number;
sortKey?: string | null;
sortOrder?: "asc" | "desc";
search?: string;
}) => {
// 🧪 Generate 100 fake users
const demoData = Array.from({ length: 100 }).map((_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
avatar: `https://i.pravatar.cc/40?img=${i + 1}`,
email: `user${i + 1}@example.com`,
role: ["Admin", "Manager", "Member"][i % 3],
status: ["Active", "Pending", "Suspended"][i % 3],
created_at: new Date(
2025,
0,
1 + (i % 28),
12,
i % 60,
).toLocaleString(),
}));
// 🔍 Searching
let filtered = demoData;
if (search && search.trim() !== "") {
const lower = search.toLowerCase();
filtered = demoData.filter((row) =>
Object.values(row).some((val) =>
String(val).toLowerCase().includes(lower),
),
);
}
// 🧭 Sorting
if (sortKey) {
filtered.sort((a, b) => {
const aVal = String(a[sortKey as keyof typeof a]);
const bVal = String(b[sortKey as keyof typeof b]);
if (aVal < bVal) return sortOrder === "asc" ? -1 : 1;
if (aVal > bVal) return sortOrder === "asc" ? 1 : -1;
return 0;
});
}
// 📄 Pagination
const total = filtered.length;
const start = (page - 1) * pageSize;
const end = start + pageSize;
const pageData = filtered.slice(start, end);
// ✅ Return object data, not HTML
return {
data: pageData,
total,
};
};
</script>
+20
View File
@@ -0,0 +1,20 @@
export interface Column<T> {
key: keyof T | string;
label: string;
sortable?: boolean;
searchable?: boolean;
render?: (value: any, row: T) => string;
width?: string;
align?: "left" | "center" | "right";
}
export interface TableProps<T> {
columns: Column<T>[];
data?: T[];
fetchData?: string;
selectable?: boolean;
search?: boolean;
striped?: boolean;
pageSize?: number
columnsRendererKey?: string
}