@pindoba/astro-table 0.0.0-alpha-20260426220647
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +12 -0
- package/index.ts +7 -0
- package/package.json +66 -0
- package/src/demos/column-visibility.astro +81 -0
- package/src/demos/custom-cell.astro +193 -0
- package/src/demos/default.astro +112 -0
- package/src/demos/expandable.astro +99 -0
- package/src/demos/filterable.astro +46 -0
- package/src/demos/grouped.astro +45 -0
- package/src/demos/headless.astro +98 -0
- package/src/demos/index.ts +11 -0
- package/src/demos/pagination.astro +45 -0
- package/src/demos/selectable.astro +92 -0
- package/src/demos/server-side.astro +155 -0
- package/src/demos/sortable.astro +33 -0
- package/src/env.d.ts +2 -0
- package/src/table.astro +514 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Table from "../table.astro";
|
|
3
|
+
import { createTable, type ColumnDef } from "@tanstack/table-core";
|
|
4
|
+
import { createTableOptions, getDefaultTableState } from "@pindoba/core-table";
|
|
5
|
+
|
|
6
|
+
interface Contact {
|
|
7
|
+
firstName: string;
|
|
8
|
+
lastName: string;
|
|
9
|
+
email: string;
|
|
10
|
+
balance: number;
|
|
11
|
+
status: "active" | "pending" | "overdue";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Seed data is pre-sorted by balance descending — the escape hatch is static
|
|
15
|
+
// (consumer-owned tables don't auto-hydrate), so we bake the order directly
|
|
16
|
+
// into the data rather than relying on TanStack's sortingFn at click time.
|
|
17
|
+
const data: Contact[] = [
|
|
18
|
+
{
|
|
19
|
+
firstName: "Donald",
|
|
20
|
+
lastName: "Knuth",
|
|
21
|
+
email: "donald@example.com",
|
|
22
|
+
balance: 21_040,
|
|
23
|
+
status: "active",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
firstName: "Ada",
|
|
27
|
+
lastName: "Lovelace",
|
|
28
|
+
email: "ada@example.com",
|
|
29
|
+
balance: 12_450,
|
|
30
|
+
status: "active",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
firstName: "Alan",
|
|
34
|
+
lastName: "Turing",
|
|
35
|
+
email: "alan@example.com",
|
|
36
|
+
balance: 8_300,
|
|
37
|
+
status: "active",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
firstName: "Linus",
|
|
41
|
+
lastName: "Torvalds",
|
|
42
|
+
email: "linus@example.com",
|
|
43
|
+
balance: 5_670,
|
|
44
|
+
status: "pending",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
firstName: "Grace",
|
|
48
|
+
lastName: "Hopper",
|
|
49
|
+
email: "grace@example.com",
|
|
50
|
+
balance: -230,
|
|
51
|
+
status: "overdue",
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
// Grouped-header columns: TanStack resolves a nested `columns` array into a
|
|
56
|
+
// multi-row `<thead>` with the right `colspan`. Pindoba's `<Table>` already
|
|
57
|
+
// iterates every header group, so the escape hatch just needs to produce the
|
|
58
|
+
// right ColumnDef shape — no wrapper changes required.
|
|
59
|
+
const columns: ColumnDef<Contact>[] = [
|
|
60
|
+
{
|
|
61
|
+
id: "contact",
|
|
62
|
+
header: "Contact",
|
|
63
|
+
columns: [
|
|
64
|
+
{ accessorKey: "firstName", header: "First" },
|
|
65
|
+
{ accessorKey: "lastName", header: "Last" },
|
|
66
|
+
{ accessorKey: "email", header: "Email" },
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "account",
|
|
71
|
+
header: "Account",
|
|
72
|
+
columns: [
|
|
73
|
+
{ accessorKey: "balance", header: "Balance" },
|
|
74
|
+
{ accessorKey: "status", header: "Status" },
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
// Sorting UI is off on purpose: the escape hatch passes a pre-built instance
|
|
80
|
+
// for SSR rendering, so there's no client-side state loop to drive sort
|
|
81
|
+
// clicks. Interactivity belongs in the normal `<Table>` path (see the
|
|
82
|
+
// `filtering`, `pagination`, etc. demos).
|
|
83
|
+
const options = createTableOptions({ data, columns, enableSorting: false });
|
|
84
|
+
const table = createTable({
|
|
85
|
+
...options,
|
|
86
|
+
state: getDefaultTableState(options),
|
|
87
|
+
onStateChange: () => {
|
|
88
|
+
/* no-op */
|
|
89
|
+
},
|
|
90
|
+
renderFallbackValue: null,
|
|
91
|
+
});
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
<Table
|
|
95
|
+
table={table}
|
|
96
|
+
bordered
|
|
97
|
+
caption="Grouped headers + balance-desc order, baked into a TanStack instance before handoff"
|
|
98
|
+
/>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { default as Default } from "./default.astro";
|
|
2
|
+
export { default as Sortable } from "./sortable.astro";
|
|
3
|
+
export { default as Pagination } from "./pagination.astro";
|
|
4
|
+
export { default as Filterable } from "./filterable.astro";
|
|
5
|
+
export { default as CustomCell } from "./custom-cell.astro";
|
|
6
|
+
export { default as Selectable } from "./selectable.astro";
|
|
7
|
+
export { default as ColumnVisibility } from "./column-visibility.astro";
|
|
8
|
+
export { default as Expandable } from "./expandable.astro";
|
|
9
|
+
export { default as Grouped } from "./grouped.astro";
|
|
10
|
+
export { default as ServerSide } from "./server-side.astro";
|
|
11
|
+
export { default as Headless } from "./headless.astro";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Table from "../table.astro";
|
|
3
|
+
import type { ColumnDef } from "@tanstack/table-core";
|
|
4
|
+
|
|
5
|
+
interface Order {
|
|
6
|
+
id: string;
|
|
7
|
+
customer: string;
|
|
8
|
+
total: string;
|
|
9
|
+
status: "pending" | "shipped" | "delivered" | "cancelled";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const statuses: Order["status"][] = [
|
|
13
|
+
"pending",
|
|
14
|
+
"shipped",
|
|
15
|
+
"delivered",
|
|
16
|
+
"cancelled",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
// Deterministic totals for SSR: avoids hydration mismatches from Math.random.
|
|
20
|
+
const data: Order[] = Array.from({ length: 47 }, (_, i) => {
|
|
21
|
+
const totalNum = Math.round(((i + 1) * 173.91) % 50000) / 100;
|
|
22
|
+
return {
|
|
23
|
+
id: `ORD-${String(1000 + i)}`,
|
|
24
|
+
customer: `Customer ${i + 1}`,
|
|
25
|
+
total: `$${totalNum.toFixed(2)}`,
|
|
26
|
+
status: statuses[i % statuses.length]!,
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const columns: ColumnDef<Order>[] = [
|
|
31
|
+
{ accessorKey: "id", header: "Order ID" },
|
|
32
|
+
{ accessorKey: "customer", header: "Customer" },
|
|
33
|
+
{ accessorKey: "total", header: "Total" },
|
|
34
|
+
{ accessorKey: "status", header: "Status" },
|
|
35
|
+
];
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
<Table
|
|
39
|
+
data={data}
|
|
40
|
+
columns={columns}
|
|
41
|
+
caption="47 orders — paginated 10 per page"
|
|
42
|
+
pageSize={10}
|
|
43
|
+
pageSizeOptions={[5, 10, 25, 50]}
|
|
44
|
+
bordered
|
|
45
|
+
/>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Table from "../table.astro";
|
|
3
|
+
import type { ColumnDef } from "@tanstack/table-core";
|
|
4
|
+
|
|
5
|
+
interface User {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
email: string;
|
|
9
|
+
role: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const data: User[] = [
|
|
13
|
+
{ id: "u1", name: "Ada Lovelace", email: "ada@example.com", role: "Admin" },
|
|
14
|
+
{ id: "u2", name: "Alan Turing", email: "alan@example.com", role: "Member" },
|
|
15
|
+
{ id: "u3", name: "Grace Hopper", email: "grace@example.com", role: "Admin" },
|
|
16
|
+
{
|
|
17
|
+
id: "u4",
|
|
18
|
+
name: "Linus Torvalds",
|
|
19
|
+
email: "linus@example.com",
|
|
20
|
+
role: "Member",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "u5",
|
|
24
|
+
name: "Donald Knuth",
|
|
25
|
+
email: "donald@example.com",
|
|
26
|
+
role: "Member",
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const columns: ColumnDef<User>[] = [
|
|
31
|
+
{ id: "select", header: "", cell: () => "", enableSorting: false },
|
|
32
|
+
{ accessorKey: "name", header: "Name" },
|
|
33
|
+
{ accessorKey: "email", header: "Email" },
|
|
34
|
+
{ accessorKey: "role", header: "Role" },
|
|
35
|
+
];
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
<div
|
|
39
|
+
data-demo="table-selectable"
|
|
40
|
+
style="display: flex; flex-direction: column; gap: 0.75rem;"
|
|
41
|
+
>
|
|
42
|
+
<p style="margin: 0; font-size: 0.875rem;">
|
|
43
|
+
<strong data-selected-count>0</strong> of {data.length} selected
|
|
44
|
+
</p>
|
|
45
|
+
|
|
46
|
+
<Table
|
|
47
|
+
data={data}
|
|
48
|
+
columns={columns}
|
|
49
|
+
enableRowSelection
|
|
50
|
+
enablePagination={false}
|
|
51
|
+
enableSorting={false}
|
|
52
|
+
getRowId={(row) => row.id}
|
|
53
|
+
caption="Row selection via the `select` column"
|
|
54
|
+
>
|
|
55
|
+
<template data-table-header-template="select">
|
|
56
|
+
<input
|
|
57
|
+
type="checkbox"
|
|
58
|
+
data-table-select-all
|
|
59
|
+
aria-label="Select all rows"
|
|
60
|
+
/>
|
|
61
|
+
</template>
|
|
62
|
+
<template data-table-cell-template="select">
|
|
63
|
+
<input type="checkbox" data-table-select-row aria-label="Select row" />
|
|
64
|
+
</template>
|
|
65
|
+
</Table>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<script>
|
|
69
|
+
function syncCounts(scope: ParentNode = document) {
|
|
70
|
+
const wrappers = scope.querySelectorAll<HTMLElement>(
|
|
71
|
+
'[data-demo="table-selectable"]',
|
|
72
|
+
);
|
|
73
|
+
wrappers.forEach((wrapper) => {
|
|
74
|
+
const counter = wrapper.querySelector<HTMLElement>(
|
|
75
|
+
"[data-selected-count]",
|
|
76
|
+
);
|
|
77
|
+
if (!counter) return;
|
|
78
|
+
const update = () => {
|
|
79
|
+
const n = wrapper.querySelectorAll(
|
|
80
|
+
'tbody tr[data-row-selected="true"]',
|
|
81
|
+
).length;
|
|
82
|
+
counter.textContent = String(n);
|
|
83
|
+
};
|
|
84
|
+
wrapper.addEventListener("change", update);
|
|
85
|
+
// Defer until mountTable has stamped the initial rows.
|
|
86
|
+
queueMicrotask(update);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
syncCounts();
|
|
91
|
+
document.addEventListener("astro:page-load", () => syncCounts());
|
|
92
|
+
</script>
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Table from "../table.astro";
|
|
3
|
+
import type { ColumnDef } from "@tanstack/table-core";
|
|
4
|
+
|
|
5
|
+
interface Log {
|
|
6
|
+
id: number;
|
|
7
|
+
level: "info" | "warn" | "error";
|
|
8
|
+
message: string;
|
|
9
|
+
timestamp: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const TOTAL_ROWS = 120;
|
|
13
|
+
|
|
14
|
+
function generateAllRows(): Log[] {
|
|
15
|
+
const levels: Log["level"][] = ["info", "warn", "error"];
|
|
16
|
+
return Array.from({ length: TOTAL_ROWS }, (_, i) => ({
|
|
17
|
+
id: i + 1,
|
|
18
|
+
level: levels[i % 3]!,
|
|
19
|
+
message: `Log entry #${i + 1}`,
|
|
20
|
+
timestamp: new Date(Date.UTC(2026, 0, 1) - i * 60_000).toISOString(),
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// SSR renders the first page of the unsorted dataset. The inline `<script>`
|
|
25
|
+
// below takes over on hydration and answers pagination/sorting changes with a
|
|
26
|
+
// fresh slice of the *full* dataset — what a real backend would do.
|
|
27
|
+
const initialRows = generateAllRows().slice(0, 10);
|
|
28
|
+
|
|
29
|
+
const columns: ColumnDef<Log>[] = [
|
|
30
|
+
{ accessorKey: "id", header: "#" },
|
|
31
|
+
{ accessorKey: "level", header: "Level" },
|
|
32
|
+
{ accessorKey: "message", header: "Message" },
|
|
33
|
+
{ accessorKey: "timestamp", header: "Timestamp" },
|
|
34
|
+
];
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
<div
|
|
38
|
+
data-demo="table-server-side"
|
|
39
|
+
style="display: flex; flex-direction: column; gap: 0.5rem;"
|
|
40
|
+
>
|
|
41
|
+
<p style="margin: 0; font-size: 0.875rem;">
|
|
42
|
+
<span data-status>{TOTAL_ROWS} total rows (server-paginated)</span>
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
<Table
|
|
46
|
+
data={initialRows}
|
|
47
|
+
columns={columns}
|
|
48
|
+
manualPagination
|
|
49
|
+
manualSorting
|
|
50
|
+
rowCount={TOTAL_ROWS}
|
|
51
|
+
caption="Mock server-side pagination + sorting"
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<script>
|
|
56
|
+
import { mountTable } from "@pindoba/core-table";
|
|
57
|
+
import type { SortingState } from "@tanstack/table-core";
|
|
58
|
+
|
|
59
|
+
type Log = {
|
|
60
|
+
id: number;
|
|
61
|
+
level: "info" | "warn" | "error";
|
|
62
|
+
message: string;
|
|
63
|
+
timestamp: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const TOTAL_ROWS = 120;
|
|
67
|
+
|
|
68
|
+
function generateAllRows(): Log[] {
|
|
69
|
+
const levels: Log["level"][] = ["info", "warn", "error"];
|
|
70
|
+
return Array.from({ length: TOTAL_ROWS }, (_, i) => ({
|
|
71
|
+
id: i + 1,
|
|
72
|
+
level: levels[i % 3]!,
|
|
73
|
+
message: `Log entry #${i + 1}`,
|
|
74
|
+
timestamp: new Date(Date.UTC(2026, 0, 1) - i * 60_000).toISOString(),
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const ALL_ROWS: Log[] = generateAllRows();
|
|
79
|
+
|
|
80
|
+
/** Mock server query: sort the full dataset, then slice the requested page. */
|
|
81
|
+
function queryPage(pi: number, ps: number, srt: SortingState): Log[] {
|
|
82
|
+
const sorted = ALL_ROWS.slice();
|
|
83
|
+
const first = srt[0];
|
|
84
|
+
if (first) {
|
|
85
|
+
const { id, desc } = first;
|
|
86
|
+
sorted.sort((a, b) => {
|
|
87
|
+
const va = (a as Record<string, unknown>)[id] as string | number;
|
|
88
|
+
const vb = (b as Record<string, unknown>)[id] as string | number;
|
|
89
|
+
if (va < vb) return desc ? 1 : -1;
|
|
90
|
+
if (va > vb) return desc ? -1 : 1;
|
|
91
|
+
return 0;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return sorted.slice(pi * ps, pi * ps + ps);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function init() {
|
|
98
|
+
const wrappers = document.querySelectorAll<HTMLElement>(
|
|
99
|
+
'[data-demo="table-server-side"]',
|
|
100
|
+
);
|
|
101
|
+
wrappers.forEach((wrapper) => {
|
|
102
|
+
const root = wrapper.querySelector<HTMLElement>(
|
|
103
|
+
'[data-component="table"][data-table-hydrate]',
|
|
104
|
+
);
|
|
105
|
+
const status = wrapper.querySelector<HTMLElement>("[data-status]");
|
|
106
|
+
if (!root) return;
|
|
107
|
+
|
|
108
|
+
// Replace the table.astro auto-mount with a manual mount so we control
|
|
109
|
+
// the `data` source via async fetches.
|
|
110
|
+
root.removeAttribute("data-table-hydrate");
|
|
111
|
+
root.setAttribute("data-table-mounted", "true");
|
|
112
|
+
|
|
113
|
+
const columns = JSON.parse(
|
|
114
|
+
root.getAttribute("data-table-columns") ?? "[]",
|
|
115
|
+
);
|
|
116
|
+
const opts = JSON.parse(root.getAttribute("data-table-options") ?? "{}");
|
|
117
|
+
|
|
118
|
+
const mount = mountTable<Log>(root, {
|
|
119
|
+
...opts,
|
|
120
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
121
|
+
columns: columns as any,
|
|
122
|
+
data: queryPage(0, opts.pageSize ?? 10, []),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
let lastKey = "";
|
|
126
|
+
const fetchPage = async () => {
|
|
127
|
+
const state = mount.store.getState();
|
|
128
|
+
const pi = state.pagination?.pageIndex ?? 0;
|
|
129
|
+
const ps = state.pagination?.pageSize ?? 10;
|
|
130
|
+
const srt = (state.sorting ?? []) as SortingState;
|
|
131
|
+
const key = `${pi}|${ps}|${srt[0]?.id ?? ""}|${srt[0]?.desc ? "d" : "a"}`;
|
|
132
|
+
if (key === lastKey) return;
|
|
133
|
+
lastKey = key;
|
|
134
|
+
if (status) status.textContent = "Loading…";
|
|
135
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
136
|
+
mount.store.actions.setData(queryPage(pi, ps, srt));
|
|
137
|
+
if (status) {
|
|
138
|
+
status.textContent = `${TOTAL_ROWS} total rows (server-paginated)`;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
mount.store.subscribe(() => {
|
|
143
|
+
void fetchPage();
|
|
144
|
+
});
|
|
145
|
+
void fetchPage();
|
|
146
|
+
|
|
147
|
+
const teardown = () => mount.destroy();
|
|
148
|
+
window.addEventListener("pagehide", teardown, { once: true });
|
|
149
|
+
document.addEventListener("astro:before-swap", teardown, { once: true });
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
init();
|
|
154
|
+
document.addEventListener("astro:page-load", init);
|
|
155
|
+
</script>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Table from "../table.astro";
|
|
3
|
+
import type { ColumnDef } from "@tanstack/table-core";
|
|
4
|
+
|
|
5
|
+
interface Row {
|
|
6
|
+
name: string;
|
|
7
|
+
stars: number;
|
|
8
|
+
language: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const data: Row[] = [
|
|
12
|
+
{ name: "Framework A", stars: 12000, language: "TypeScript" },
|
|
13
|
+
{ name: "Framework B", stars: 48000, language: "JavaScript" },
|
|
14
|
+
{ name: "Framework C", stars: 7400, language: "Rust" },
|
|
15
|
+
{ name: "Framework D", stars: 23500, language: "Go" },
|
|
16
|
+
{ name: "Framework E", stars: 31200, language: "Python" },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const columns: ColumnDef<Row>[] = [
|
|
20
|
+
{ accessorKey: "name", header: "Name" },
|
|
21
|
+
{ accessorKey: "stars", header: "★ Stars" },
|
|
22
|
+
{ accessorKey: "language", header: "Language" },
|
|
23
|
+
];
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
<Table
|
|
27
|
+
data={data}
|
|
28
|
+
columns={columns}
|
|
29
|
+
striped
|
|
30
|
+
caption="Click any header to sort (ascending, descending, unsorted)"
|
|
31
|
+
initialState={{ sorting: [{ id: "stars", desc: true }] }}
|
|
32
|
+
enablePagination={false}
|
|
33
|
+
/>
|
package/src/env.d.ts
ADDED