@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.
@@ -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
@@ -0,0 +1,2 @@
1
+ // eslint-disable-next-line @typescript-eslint/triple-slash-reference
2
+ /// <reference path="../.astro/types.d.ts" />