@kitr/table 0.0.0-stage → 0.3.0

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Platon Podkopov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,15 @@
1
- # Temporary Holding Version
1
+ # @kitr/table
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Tables.** Show rows of data with sorting, paging and filters that stay in the URL.
4
+
5
+ ```sh
6
+ pnpm kitr add table
7
+ ```
8
+
9
+ `kitr add` installs the package and wires it into your app: the provider in `kitrrc.ts`, its config file and its `AGENTS.md` section. Import it from `@kitr/table`.
10
+
11
+ Built on @tanstack/react-table.
12
+
13
+ Read the guide at https://kitr.dev/packages/table.
14
+
15
+ Part of [kitr](https://kitr.dev), the TypeScript framework where everything is an add-on. MIT licensed.
@@ -0,0 +1,97 @@
1
+ import { ColumnFiltersState, PaginationState, SortingState, Updater } from "@tanstack/react-table";
2
+ export * from "@tanstack/react-table";
3
+ /**
4
+ * The feature set kitr's DataTable and `useUrlTableState` assume: sorting,
5
+ * pagination, column filters and column visibility. TanStack Table v9 is feature-sliced, so a
6
+ * table only ships the code for the features you register. Extend with
7
+ * `tableFeatures({ ...dataTableFeatures, rowSelectionFeature })`.
8
+ */
9
+ export declare const dataTableFeatures: {
10
+ rowSortingFeature: import("@tanstack/react-table").TableFeature;
11
+ rowPaginationFeature: import("@tanstack/react-table").TableFeature;
12
+ columnFilteringFeature: import("@tanstack/react-table").TableFeature;
13
+ columnVisibilityFeature: import("@tanstack/react-table").TableFeature;
14
+ };
15
+ /** The type of `dataTableFeatures`, for `ColumnDef<DataTableFeatures, Order>` and `useTable` generics. */
16
+ export type DataTableFeatures = typeof dataTableFeatures;
17
+ /**
18
+ * `dataTableFeatures` plus the browser-side row models: for a list that arrives in one response,
19
+ * the table sorts, filters and pages it itself. Turn the manual flags off with it
20
+ * (`useUrlTableState({ ..., manual: false })` does), or nothing changes when a header is clicked.
21
+ */
22
+ export declare const clientTableFeatures: {
23
+ sortedRowModel: (table: import("@tanstack/react-table").Table<any, any>) => () => import("@tanstack/react-table").RowModel<any, any>;
24
+ paginatedRowModel: (table: import("@tanstack/react-table").Table<any, any>) => () => import("@tanstack/react-table").RowModel<any, any>;
25
+ filteredRowModel: (table: import("@tanstack/react-table").Table<any, any>) => () => import("@tanstack/react-table").RowModel<any, any>;
26
+ sortFns: {
27
+ alphanumeric: import("@tanstack/react-table").CreatedSortFn<any, any>;
28
+ text: import("@tanstack/react-table").CreatedSortFn<any, any>;
29
+ datetime: import("@tanstack/react-table").CreatedSortFn<any, any>;
30
+ basic: import("@tanstack/react-table").CreatedSortFn<any, any>;
31
+ };
32
+ filterFns: {
33
+ includesString: import("@tanstack/react-table").CreatedFilterFn<any, any>;
34
+ equalsString: import("@tanstack/react-table").CreatedFilterFn<any, any>;
35
+ equals: import("@tanstack/react-table").CreatedFilterFn<any, any>;
36
+ weakEquals: import("@tanstack/react-table").CreatedFilterFn<any, any>;
37
+ inNumberRange: import("@tanstack/react-table").CreatedFilterFn<any, any>;
38
+ arrIncludes: import("@tanstack/react-table").CreatedFilterFn<any, any>;
39
+ arrIncludesAll: import("@tanstack/react-table").CreatedFilterFn<any, any>;
40
+ arrIncludesSome: import("@tanstack/react-table").CreatedFilterFn<any, any>;
41
+ };
42
+ rowSortingFeature: import("@tanstack/react-table").TableFeature;
43
+ rowPaginationFeature: import("@tanstack/react-table").TableFeature;
44
+ columnFilteringFeature: import("@tanstack/react-table").TableFeature;
45
+ columnVisibilityFeature: import("@tanstack/react-table").TableFeature;
46
+ };
47
+ /** The type of `clientTableFeatures`. */
48
+ export type ClientTableFeatures = typeof clientTableFeatures;
49
+ /** The search-param shape kitr tables use. Add it to your route's `validateSearch`. */
50
+ export interface TableSearch {
51
+ /** One-based page number. Absent means page 1. */
52
+ page?: number;
53
+ /** Rows per page. Absent means the default page size. */
54
+ size?: number;
55
+ /** Sort columns, comma-separated, `-` for descending: `-total,customer`. */
56
+ sort?: string;
57
+ /** Column filters as column id to string value: `{ status: 'open' }`. */
58
+ filter?: Record<string, string>;
59
+ }
60
+ /** Options for `useUrlTableState`: the route's current search, a way to change it and the page size. */
61
+ export interface UrlTableStateOptions {
62
+ /** The current table search params, usually `Route.useSearch()`. */
63
+ search: TableSearch;
64
+ /** Merge a patch into the URL, e.g. `(patch) => navigate({ search: (s) => ({ ...s, ...patch }) })`. */
65
+ setSearch: (patch: Partial<TableSearch>) => void;
66
+ /** Rows per page when the URL has no `size`, and the size left out of the URL. Default 20. */
67
+ defaultPageSize?: number;
68
+ /**
69
+ * `true` (default): the server sorts, filters and pages, the table shows what came back.
70
+ * `false`: the table does it in the browser; use it with `clientTableFeatures`.
71
+ */
72
+ manual?: boolean;
73
+ }
74
+ /**
75
+ * Bind sorting, pagination and column filters to URL search params so a
76
+ * filtered view is shareable by link. Router-agnostic; pass the route's search
77
+ * and a setter. Spread the result into `useTable` (with `dataTableFeatures` or a superset).
78
+ */
79
+ export declare function useUrlTableState({ search, setSearch, defaultPageSize, manual }: UrlTableStateOptions): {
80
+ state: {
81
+ sorting: SortingState;
82
+ pagination: PaginationState;
83
+ columnFilters: ColumnFiltersState;
84
+ };
85
+ onSortingChange: (updater: Updater<SortingState>) => void;
86
+ onPaginationChange: (updater: Updater<PaginationState>) => void;
87
+ onColumnFiltersChange: (updater: Updater<ColumnFiltersState>) => void;
88
+ manualSorting: boolean;
89
+ manualPagination: boolean;
90
+ manualFiltering: boolean;
91
+ };
92
+ /** `-name,createdAt` ⇄ `[{ id: 'name', desc: true }, { id: 'createdAt', desc: false }]` */
93
+ export declare function parseSort(sort?: string): SortingState;
94
+ /** Turns TanStack's sorting state into the URL's `sort` string: `[{ id: 'total', desc: true }]` becomes `-total`. */
95
+ export declare function formatSort(sorting: SortingState): string;
96
+ /** Validate raw search params into `TableSearch`. Plug into `validateSearch` or wrap with zod. */
97
+ export declare function parseTableSearch(raw: Record<string, unknown>): TableSearch;
package/dist/index.js ADDED
@@ -0,0 +1,135 @@
1
+ import { columnFilteringFeature, columnVisibilityFeature, createFilteredRowModel, createPaginatedRowModel, createSortedRowModel, filterFn_arrIncludes, filterFn_arrIncludesAll, filterFn_arrIncludesSome, filterFn_equals, filterFn_equalsString, filterFn_inNumberRange, filterFn_includesString, filterFn_weakEquals, rowPaginationFeature, rowSortingFeature, sortFn_alphanumeric, sortFn_basic, sortFn_datetime, sortFn_text, tableFeatures } from "@tanstack/react-table";
2
+ import { useCallback, useMemo } from "react";
3
+ export * from "@tanstack/react-table";
4
+ // src/features.ts
5
+ /**
6
+ * The feature set kitr's DataTable and `useUrlTableState` assume: sorting,
7
+ * pagination, column filters and column visibility. TanStack Table v9 is feature-sliced, so a
8
+ * table only ships the code for the features you register. Extend with
9
+ * `tableFeatures({ ...dataTableFeatures, rowSelectionFeature })`.
10
+ */
11
+ const dataTableFeatures = tableFeatures({
12
+ rowSortingFeature,
13
+ rowPaginationFeature,
14
+ columnFilteringFeature,
15
+ columnVisibilityFeature
16
+ });
17
+ /**
18
+ * `dataTableFeatures` plus the browser-side row models: for a list that arrives in one response,
19
+ * the table sorts, filters and pages it itself. Turn the manual flags off with it
20
+ * (`useUrlTableState({ ..., manual: false })` does), or nothing changes when a header is clicked.
21
+ */
22
+ const clientTableFeatures = tableFeatures({
23
+ ...dataTableFeatures,
24
+ sortedRowModel: createSortedRowModel(),
25
+ paginatedRowModel: createPaginatedRowModel(),
26
+ filteredRowModel: createFilteredRowModel(),
27
+ sortFns: {
28
+ alphanumeric: sortFn_alphanumeric,
29
+ text: sortFn_text,
30
+ datetime: sortFn_datetime,
31
+ basic: sortFn_basic
32
+ },
33
+ filterFns: {
34
+ includesString: filterFn_includesString,
35
+ equalsString: filterFn_equalsString,
36
+ equals: filterFn_equals,
37
+ weakEquals: filterFn_weakEquals,
38
+ inNumberRange: filterFn_inNumberRange,
39
+ arrIncludes: filterFn_arrIncludes,
40
+ arrIncludesAll: filterFn_arrIncludesAll,
41
+ arrIncludesSome: filterFn_arrIncludesSome
42
+ }
43
+ });
44
+ // src/url-state.ts
45
+ /**
46
+ * Bind sorting, pagination and column filters to URL search params so a
47
+ * filtered view is shareable by link. Router-agnostic; pass the route's search
48
+ * and a setter. Spread the result into `useTable` (with `dataTableFeatures` or a superset).
49
+ */
50
+ function useUrlTableState({ search, setSearch, defaultPageSize = 20, manual = true }) {
51
+ const sorting = useMemo(() => parseSort(search.sort), [search.sort]);
52
+ const pagination = useMemo(() => ({
53
+ pageIndex: Math.max(0, (search.page ?? 1) - 1),
54
+ pageSize: search.size ?? defaultPageSize
55
+ }), [
56
+ search.page,
57
+ search.size,
58
+ defaultPageSize
59
+ ]);
60
+ const columnFilters = useMemo(() => Object.entries(search.filter ?? {}).map(([id, value]) => ({
61
+ id,
62
+ value
63
+ })), [search.filter]);
64
+ const onSortingChange = useCallback((updater) => {
65
+ const next = typeof updater === "function" ? updater(sorting) : updater;
66
+ setSearch({
67
+ sort: next.length ? formatSort(next) : void 0,
68
+ page: void 0
69
+ });
70
+ }, [sorting, setSearch]);
71
+ const onPaginationChange = useCallback((updater) => {
72
+ const next = typeof updater === "function" ? updater(pagination) : updater;
73
+ setSearch({
74
+ page: next.pageIndex === 0 ? void 0 : next.pageIndex + 1,
75
+ size: next.pageSize === defaultPageSize ? void 0 : next.pageSize
76
+ });
77
+ }, [
78
+ pagination,
79
+ setSearch,
80
+ defaultPageSize
81
+ ]);
82
+ const onColumnFiltersChange = useCallback((updater) => {
83
+ const next = typeof updater === "function" ? updater(columnFilters) : updater;
84
+ const filter = {};
85
+ for (const f of next) if (f.value !== void 0 && f.value !== "") filter[f.id] = String(f.value);
86
+ setSearch({
87
+ filter: Object.keys(filter).length ? filter : void 0,
88
+ page: void 0
89
+ });
90
+ }, [columnFilters, setSearch]);
91
+ return {
92
+ state: {
93
+ sorting,
94
+ pagination,
95
+ columnFilters
96
+ },
97
+ onSortingChange,
98
+ onPaginationChange,
99
+ onColumnFiltersChange,
100
+ manualSorting: manual,
101
+ manualPagination: manual,
102
+ manualFiltering: manual
103
+ };
104
+ }
105
+ /** `-name,createdAt` ⇄ `[{ id: 'name', desc: true }, { id: 'createdAt', desc: false }]` */
106
+ function parseSort(sort) {
107
+ if (!sort) return [];
108
+ return sort.split(",").filter(Boolean).map((part) => part.startsWith("-") ? {
109
+ id: part.slice(1),
110
+ desc: true
111
+ } : {
112
+ id: part,
113
+ desc: false
114
+ });
115
+ }
116
+ /** Turns TanStack's sorting state into the URL's `sort` string: `[{ id: 'total', desc: true }]` becomes `-total`. */
117
+ function formatSort(sorting) {
118
+ return sorting.map((s) => s.desc ? `-${s.id}` : s.id).join(",");
119
+ }
120
+ /** Validate raw search params into `TableSearch`. Plug into `validateSearch` or wrap with zod. */
121
+ function parseTableSearch(raw) {
122
+ const out = {};
123
+ const page = Number(raw.page);
124
+ if (Number.isInteger(page) && page > 0) out.page = page;
125
+ const size = Number(raw.size);
126
+ if (Number.isInteger(size) && size > 0 && size <= 500) out.size = size;
127
+ if (typeof raw.sort === "string" && /^-?[\w.]+(,-?[\w.]+)*$/.test(raw.sort)) out.sort = raw.sort;
128
+ if (raw.filter && typeof raw.filter === "object") {
129
+ const filter = {};
130
+ for (const [k, v] of Object.entries(raw.filter)) if (typeof v === "string") filter[k] = v;
131
+ if (Object.keys(filter).length) out.filter = filter;
132
+ }
133
+ return out;
134
+ }
135
+ export { clientTableFeatures, dataTableFeatures, formatSort, parseSort, parseTableSearch, useUrlTableState };
package/dist/kitr.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ /** `kitr add table`: TanStack Table with sorting, pagination and filters bound to URL search params. */
2
+ declare const _default: {
3
+ readonly name: "table";
4
+ readonly client: true;
5
+ readonly add: (ctx: import("@kitr/core/package").AddContext) => void;
6
+ readonly remove: (ctx: import("@kitr/core/package").AddContext) => void;
7
+ readonly wraps: {
8
+ readonly '@tanstack/react-table': "@kitr/table";
9
+ };
10
+ readonly eject: {
11
+ readonly '@kitr/table': "@tanstack/react-table";
12
+ };
13
+ readonly vite: {
14
+ readonly optimizeDeps: ["@tanstack/react-table"];
15
+ };
16
+ };
17
+ export { _default as default };
package/dist/kitr.js ADDED
@@ -0,0 +1,19 @@
1
+ import { definePackage } from "@kitr/core/package";
2
+ // src/kitr.ts
3
+ /** `kitr add table`: TanStack Table with sorting, pagination and filters bound to URL search params. */
4
+ var kitr_default = definePackage({
5
+ name: "table",
6
+ client: true,
7
+ add(ctx) {
8
+ ctx.deps.add("@tanstack/react-table");
9
+ ctx.agents.add("table", `- Tables: \`validateSearch: parseTableSearch\` on the route, \`useUrlTableState({ search, setSearch })\` spread into \`useReactTable\` or the registry \`<DataTable>\`. Sorting/paging live in the URL, so views are shareable.`);
10
+ },
11
+ remove(ctx) {
12
+ ctx.deps.remove("@tanstack/react-table");
13
+ ctx.agents.remove("table");
14
+ },
15
+ wraps: { "@tanstack/react-table": "@kitr/table" },
16
+ eject: { "@kitr/table": "@tanstack/react-table" },
17
+ vite: { optimizeDeps: ["@tanstack/react-table"] }
18
+ });
19
+ export { kitr_default as default };
package/docs/api.md ADDED
@@ -0,0 +1,68 @@
1
+ ---
2
+ title: API reference
3
+ description: Every export of @kitr/table.
4
+ order: 2
5
+ ---
6
+
7
+ # API reference
8
+
9
+ `@kitr/table` re-exports everything from `@tanstack/react-table` v9 and adds the following.
10
+
11
+ ## useUrlTableState
12
+
13
+ ```ts
14
+ function useUrlTableState(options: {
15
+ search: TableSearch
16
+ setSearch: (patch: Partial<TableSearch>) => void
17
+ defaultPageSize?: number // default 20
18
+ }): {
19
+ state: { sorting; pagination; columnFilters }
20
+ onSortingChange
21
+ onPaginationChange
22
+ onColumnFiltersChange
23
+ manualSorting: true
24
+ manualPagination: true
25
+ manualFiltering: true
26
+ }
27
+ ```
28
+
29
+ Spread the result into `useTable` or `<DataTable>`. Router-agnostic: any router that can read and patch search params works.
30
+
31
+ ## TableSearch
32
+
33
+ ```ts
34
+ interface TableSearch {
35
+ page?: number // one-based
36
+ size?: number
37
+ sort?: string // "name,-createdAt"
38
+ filter?: Record<string, string>
39
+ }
40
+ ```
41
+
42
+ ## parseTableSearch
43
+
44
+ ```ts
45
+ function parseTableSearch(raw: Record<string, unknown>): TableSearch
46
+ ```
47
+
48
+ For a route's `validateSearch`. Keeps positive integer pages, sizes up to 500, well-formed sort strings and string filters; drops everything else.
49
+
50
+ ## parseSort and formatSort
51
+
52
+ ```ts
53
+ parseSort('-name,createdAt') // [{ id: 'name', desc: true }, { id: 'createdAt', desc: false }]
54
+ formatSort([{ id: 'name', desc: true }]) // '-name'
55
+ ```
56
+
57
+ ## dataTableFeatures
58
+
59
+ ```ts
60
+ const dataTableFeatures = tableFeatures({
61
+ rowSortingFeature,
62
+ rowPaginationFeature,
63
+ columnFilteringFeature,
64
+ columnVisibilityFeature,
65
+ })
66
+ ```
67
+
68
+ The features `<DataTable>` and `useUrlTableState` assume. TanStack Table v9 only bundles the features you register. Add more with `tableFeatures({ ...dataTableFeatures, rowSelectionFeature })`.