puppertino-table 0.1.0 → 0.2.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/CHANGELOG.md +16 -1
- package/README.md +122 -9
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -0
- package/dist/resize-handle.d.ts +5 -3
- package/dist/resize-handle.js +2 -1
- package/dist/table.d.ts +24 -19
- package/dist/table.js +17 -35
- package/dist/use-column-resize.d.ts +2 -2
- package/dist/use-column-resize.js +124 -90
- package/dist/use-puppertino-table.d.ts +22 -0
- package/dist/use-puppertino-table.js +48 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.
|
|
3
|
+
## 0.2.0 — 2026-10-12
|
|
4
|
+
|
|
5
|
+
- Add typed row/cell DOM customization callbacks and a leading-rows slot to the complete table.
|
|
6
|
+
- Expose `usePuppertinoTable` for shared sorting, selection and persisted-sizing initialization in custom layouts.
|
|
7
|
+
- Share resize gesture state and global listeners per table, with safe active-handle removal and coordinated columns.
|
|
8
|
+
- Support uncontrolled sorting/selection with initial values and change notifications; controlled state without callbacks is read-only.
|
|
9
|
+
- Supply typed body context and data attributes to customization callbacks; infer primitive resize labels from headers.
|
|
10
|
+
- Document complete-component and primitive integrations, including server-owned pagination.
|
|
11
|
+
|
|
12
|
+
## 0.1.1 — 2026-10-12
|
|
13
|
+
|
|
14
|
+
- Correct npm installation and publication-status documentation.
|
|
15
|
+
- Add a guarded GitHub Release publishing workflow with exact tag, commit, version and tarball validation.
|
|
16
|
+
- Run adversarial release-guard tests in CI and verify registry consumers after publishing.
|
|
17
|
+
|
|
18
|
+
## 0.1.0 — 2026-10-12
|
|
4
19
|
|
|
5
20
|
- Publish the requested unscoped `puppertino-table` package.
|
|
6
21
|
- Restore pre-gesture widths when standalone resize controls unmount or their cancellation settings change.
|
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A generic React table styled with [Puppertino](https://puppertino.com/) and powered by [TanStack Table](https://tanstack.com/table). Bring your own data, columns and cell renderers.
|
|
4
4
|
|
|
5
|
+
- Complete component customization and a reusable state hook for custom layouts.
|
|
6
|
+
- One shared resize controller per table, including standalone handles.
|
|
5
7
|
- Typed column definitions for arbitrary row shapes, including grouped headers.
|
|
6
8
|
- Host-controlled sorting by default, with optional local sorting.
|
|
7
9
|
- Checkbox selection with Shift-click ranges and keyboard toggling.
|
|
@@ -12,10 +14,10 @@ A generic React table styled with [Puppertino](https://puppertino.com/) and powe
|
|
|
12
14
|
|
|
13
15
|
## Install
|
|
14
16
|
|
|
15
|
-
The package includes built ESM and TypeScript declarations.
|
|
17
|
+
The package includes built ESM and TypeScript declarations. Install it from npm:
|
|
16
18
|
|
|
17
19
|
```sh
|
|
18
|
-
pnpm add puppertino-table@0.
|
|
20
|
+
pnpm add puppertino-table@0.2.0 @tanstack/react-table@9.2.8 @codedgar/puppertino@2 react react-dom
|
|
19
21
|
```
|
|
20
22
|
|
|
21
23
|
React, TanStack Table and Puppertino are peer dependencies; the library does not bundle another copy of them. For Git-based installation, use `github:kahwee/puppertino-table` and pin a commit or tag for reproducibility.
|
|
@@ -59,6 +61,8 @@ export function Example() {
|
|
|
59
61
|
}
|
|
60
62
|
```
|
|
61
63
|
|
|
64
|
+
For simpler tables, omit `sorting` and `onSortingChange` to let the library own sort state; set `manualSorting={false}` for local ordering. `initialSorting` and `initialRowSelection` seed uncontrolled state on mount. You can provide change callbacks as notifications while leaving state uncontrolled. If you provide controlled `sorting` or `rowSelection` without its matching callback, that state is read-only. Do not switch between controlled and uncontrolled ownership while a table is mounted.
|
|
65
|
+
|
|
62
66
|
The example uses local sorting and uncontrolled checkbox selection. Pass both `rowSelection` and `onRowSelectionChange` for controlled selection. Controlled selection without a change callback is read-only. Keep column definitions and data references stable when possible.
|
|
63
67
|
|
|
64
68
|
### Sorting and pagination
|
|
@@ -73,9 +77,118 @@ Drag a header's right edge. Double-click or Enter resets that column to its defi
|
|
|
73
77
|
|
|
74
78
|
`cancelMediaQuery` is optional: set it when your layout removes the table at a responsive breakpoint. A narrow table that remains visible can simply scroll horizontally.
|
|
75
79
|
|
|
76
|
-
###
|
|
80
|
+
### Choose an integration path
|
|
81
|
+
|
|
82
|
+
Use **`PuppertinoTable`** for the complete semantic table: headers, sort buttons, optional selection, bounded resize handles, scrolling and memoized data rows. Customize it with `getRowProps(row)`, `getCellProps(cell)` and `leadingRows` before building a separate layout.
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
<PuppertinoTable
|
|
86
|
+
data={items} columns={columns} getRowId={item => item.id}
|
|
87
|
+
sorting={sorting} onSortingChange={setSorting} ariaLabel="Items" selectable
|
|
88
|
+
getRowProps={row => ({
|
|
89
|
+
className: row.id === activeId ? 'active-row' : undefined,
|
|
90
|
+
'aria-current': row.id === activeId ? 'true' : undefined,
|
|
91
|
+
onClick: () => setActiveId(row.id),
|
|
92
|
+
onContextMenu: event => { event.preventDefault(); openMenu(row.original, event.clientX, event.clientY); },
|
|
93
|
+
})}
|
|
94
|
+
getCellProps={cell => ({ title: String(cell.getValue() ?? '') })}
|
|
95
|
+
leadingRows={(_table, context) => (
|
|
96
|
+
<tr><td colSpan={context.columnCount}>
|
|
97
|
+
<button type="button" onClick={openFolder}>Open folder</button>
|
|
98
|
+
</td></tr>
|
|
99
|
+
)}
|
|
100
|
+
/>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Callbacks receive typed TanStack row/cell objects, including `original`, IDs and state methods. Their second argument is `PuppertinoBodyContext`: `{ table, columnCount, selectable, disabled }`. Typed `data-*` attributes are supported on row and cell props; return `undefined` to leave a row or cell unchanged. Row props are applied to data `<tr>` elements and cell props to data `<td>` elements. The selection cell stays library-owned. Custom row classes are combined with `p-selected`; selection checkbox clicks do not bubble to the row. The library retains row/cell children and React keys. Keep callbacks stable when possible so sizing updates can reuse the memoized body. Callbacks can attach events, styles and DOM attributes; the app owns context-menu rendering and keyboard access to its actions.
|
|
104
|
+
|
|
105
|
+
`leadingRows` accepts React content or a function receiving the table. Its second argument is the same body context, with `columnCount` including the selection column. Return semantic `<tr>` elements with `<th>`/`<td>` children. It renders inside `<tbody>` before data rows, including when there are no data rows; the empty-state row still renders. Use `context.columnCount` for full-width row `colSpan`. Leading rows are app-owned: they do not enter the data row model, sorting, selection ranges or the row/cell callbacks.
|
|
106
|
+
|
|
107
|
+
Use **`usePuppertinoTable` and the primitives** when you own the table markup, selection controls or header layout. The hook returns a `PuppertinoTableInstance` and shares the complete component's column bounds, persisted initial sizing, sorting ownership and controlled/uncontrolled selection rules. It accepts the state/data options from `PuppertinoTable`, without rendering options such as `ariaLabel` or `leadingRows`.
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
import { useState } from 'react';
|
|
111
|
+
import type { SortingState } from '@tanstack/react-table';
|
|
112
|
+
import { usePuppertinoTable, ColumnResizeHandle } from 'puppertino-table';
|
|
113
|
+
|
|
114
|
+
function CustomItems() {
|
|
115
|
+
const [sorting, setSorting] = useState<SortingState>([]);
|
|
116
|
+
const table = usePuppertinoTable({
|
|
117
|
+
data: items, columns, getRowId: item => item.id,
|
|
118
|
+
sorting, onSortingChange: setSorting, manualSorting: false, selectable: true,
|
|
119
|
+
storageKey: 'my-app.custom-items.columns.v1',
|
|
120
|
+
});
|
|
121
|
+
return <table.Subscribe source={table.atoms.columnSizing}>{() => (
|
|
122
|
+
<table className="p-table pt-table" aria-label="Items" style={{ width: table.getTotalSize() }}>
|
|
123
|
+
<colgroup>{table.getAllLeafColumns().map(column => <col key={column.id} style={{ width: column.getSize() }} />)}</colgroup>
|
|
124
|
+
<thead>{table.getHeaderGroups().map(group => <tr key={group.id}>{group.headers.map(header => (
|
|
125
|
+
<th key={header.id} colSpan={header.colSpan} scope={header.column.columns.length ? 'colgroup' : 'col'}
|
|
126
|
+
aria-sort={header.column.getIsSorted() === 'asc' ? 'ascending' : header.column.getIsSorted() === 'desc' ? 'descending' : undefined}>
|
|
127
|
+
{!header.isPlaceholder && (header.column.getCanSort()
|
|
128
|
+
? <button type="button" onClick={() => header.column.toggleSorting()}><table.FlexRender header={header} /></button>
|
|
129
|
+
: <table.FlexRender header={header} />)}
|
|
130
|
+
<ColumnResizeHandle table={table} header={header} label={header.column.id}
|
|
131
|
+
options={{ storageKey: 'my-app.custom-items.columns.v1' }} />
|
|
132
|
+
</th>
|
|
133
|
+
))}</tr>)}</thead>
|
|
134
|
+
<tbody>{table.getRowModel().rows.map(row => <tr key={row.id}>
|
|
135
|
+
{row.getAllCells().map((cell, index) => <td key={cell.id}>
|
|
136
|
+
{index === 0 && <input type="checkbox" aria-label={`Select ${row.original.name}`}
|
|
137
|
+
checked={row.getIsSelected()} disabled={!row.getCanSelect()} onChange={row.getToggleSelectedHandler()} />}
|
|
138
|
+
<table.FlexRender cell={cell} />
|
|
139
|
+
</td>)}
|
|
140
|
+
</tr>)}</tbody>
|
|
141
|
+
</table>
|
|
142
|
+
)}</table.Subscribe>;
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The hook initializes sizing but does not install resize listeners. `ColumnResizeHandle` infers its accessible label from a string header or column ID; pass `label` for a custom or translated name. Its props type is exported as `ColumnResizeHandleProps`. `ColumnResizeHandle` and `useColumnResize` automatically share a controller keyed by the table's sizing store, with one pair of global keydown/blur listeners and one active gesture. The initiating handle's options govern that gesture; pass consistent storage and responsive settings to all handles. Removing the active handle, disabling it or changing its resize settings cancels its preview. Removing other handles leaves the gesture running. Separate tables remain independent. Pass `options={{ enabled: false }}` to hide a primitive handle when your layout is disabled; the hook's `disabled` controls selection, while primitive UI controls remain yours to disable.
|
|
147
|
+
|
|
148
|
+
For more specialized state wiring, the existing `puppertinoTableFeatures`, `PuppertinoTableInstance`, sizing helpers and TanStack `useTable` remain available. In custom layouts, memoize your row body separately from the `columnSizing` subscription to avoid rerendering cells during drags.
|
|
149
|
+
|
|
150
|
+
### Server-paginated complete table
|
|
151
|
+
|
|
152
|
+
The server owns pagination and order. This example receives one page and its total from the host's loader; it makes no assumption about an API URL. `loadPage` must honor its abort signal. Updating the sort resets the page; stale requests cannot replace newer results. Use stable IDs across pages and keep selection state in the host if selections must survive page changes.
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
import { useEffect, useState } from 'react';
|
|
156
|
+
import type { SortingState, RowSelectionState } from '@tanstack/react-table';
|
|
157
|
+
import { PuppertinoTable } from 'puppertino-table';
|
|
158
|
+
|
|
159
|
+
type PageLoader = (query: { page: number; pageSize: number; sorting: SortingState; signal: AbortSignal }) => Promise<{ rows: Item[]; total: number }>;
|
|
160
|
+
|
|
161
|
+
function ServerItems({ loadPage }: { loadPage: PageLoader }) {
|
|
162
|
+
const [query, setQuery] = useState({ page: 0, sorting: [] as SortingState });
|
|
163
|
+
const [selection, setSelection] = useState<RowSelectionState>({});
|
|
164
|
+
const [result, setResult] = useState<{ rows: Item[]; total: number }>({ rows: [], total: 0 });
|
|
165
|
+
const [loading, setLoading] = useState(true);
|
|
166
|
+
const [error, setError] = useState('');
|
|
167
|
+
const pageSize = 25;
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
const controller = new AbortController();
|
|
170
|
+
setLoading(true); setError('');
|
|
171
|
+
loadPage({ ...query, pageSize, signal: controller.signal })
|
|
172
|
+
.then(value => { if (!controller.signal.aborted) setResult(value); })
|
|
173
|
+
.catch(() => { if (!controller.signal.aborted) setError('Could not load items.'); })
|
|
174
|
+
.finally(() => { if (!controller.signal.aborted) setLoading(false); });
|
|
175
|
+
return () => controller.abort();
|
|
176
|
+
}, [loadPage, query]);
|
|
177
|
+
return <>
|
|
178
|
+
{error && <p role="alert">{error}</p>}
|
|
179
|
+
<PuppertinoTable data={result.rows} columns={columns} getRowId={item => item.id} ariaLabel="Items"
|
|
180
|
+
manualSorting sorting={query.sorting}
|
|
181
|
+
onSortingChange={update => setQuery(previous => ({ page: 0, sorting: typeof update === 'function' ? update(previous.sorting) : update }))}
|
|
182
|
+
selectable rowSelection={selection} onRowSelectionChange={setSelection} disabled={loading}
|
|
183
|
+
emptyContent={loading ? 'Loading items…' : 'No items'} />
|
|
184
|
+
<p role="status">{loading ? 'Loading items…' : `${result.total} items`}</p>
|
|
185
|
+
<button type="button" disabled={loading || query.page === 0} onClick={() => setQuery(previous => ({ ...previous, page: previous.page - 1 }))}>Previous page</button>
|
|
186
|
+
<button type="button" disabled={loading || (query.page + 1) * pageSize >= result.total} onClick={() => setQuery(previous => ({ ...previous, page: previous.page + 1 }))}>Next page</button>
|
|
187
|
+
</>;
|
|
188
|
+
}
|
|
189
|
+
```
|
|
77
190
|
|
|
78
|
-
|
|
191
|
+
Keep `loadPage` stable (for example, a module-level function or `useCallback`). Selection IDs may include unloaded pages, so counts and bulk actions should use the host's selection state, not just the current row model. Use the same data, query and selection options with `usePuppertinoTable` for a server-paginated custom layout. Leave `manualSorting` enabled in both paths so a loaded page never gets re-sorted as if it were the complete dataset.
|
|
79
192
|
|
|
80
193
|
The package adds scoped `.pt-*` enhancements to Puppertino's `p-table` styles. Import Puppertino's CSS in your application, then the package stylesheet. Theme tokens remain owned by Puppertino and your application. Include Puppertino's dark-mode stylesheet when using its dark theme.
|
|
81
194
|
|
|
@@ -92,7 +205,7 @@ pnpm dev # standalone 267-row fictional-data demo
|
|
|
92
205
|
pnpm build:storybook # static Storybook in storybook-static/
|
|
93
206
|
```
|
|
94
207
|
|
|
95
|
-
Stories cover local sorting, host-owned order, range selection, keyboard resizing, dark mode, narrow layouts, empty/disabled/read-only states, grouped headers,
|
|
208
|
+
Stories cover local sorting, host-owned order, range selection, keyboard resizing, dark mode, narrow layouts, empty/disabled/read-only states, grouped headers, a different row schema with currency cells, customized/leading rows, and a custom hook layout. Story controls expose selection, resizing, disabled state and sorting ownership.
|
|
96
209
|
|
|
97
210
|
## Verification
|
|
98
211
|
|
|
@@ -100,17 +213,17 @@ Stories cover local sorting, host-owned order, range selection, keyboard resizin
|
|
|
100
213
|
pnpm check
|
|
101
214
|
```
|
|
102
215
|
|
|
103
|
-
Checks include TypeScript, preference/schema unit tests, library/demo/Storybook builds, and Chromium regression tests. The browser suite exercises 512 repeated pointer drags, width limits, reset/cancellation/persistence, selection, unmount/disabled recovery, standalone-control removal and grouped-header resizing, all 267 demo rows across six pages, six light/dark accessibility layouts, all
|
|
216
|
+
Checks include TypeScript, preference/schema unit tests, library/demo/Storybook builds, and Chromium regression tests. The browser suite exercises 512 repeated pointer drags, width limits, reset/cancellation/persistence, selection, unmount/disabled recovery, standalone-control removal and grouped-header resizing, all 267 demo rows across six pages, six light/dark accessibility layouts, all seventeen Storybook stories, and tarball installation with typed imports, server rendering and CSS exports in fresh React 18 and React 19 consumers. Set `PLAYWRIGHT_CHROMIUM_EXECUTABLE` to a system Chromium path, or install a browser with `pnpm exec playwright install chromium`.
|
|
104
217
|
|
|
105
218
|
GitHub Actions runs the same checks and uploads Storybook plus browser evidence as an artifact. Browser tests use fictional data and do not claim Safari, physical-device or latency coverage.
|
|
106
219
|
|
|
107
|
-
## npm
|
|
220
|
+
## npm releases
|
|
108
221
|
|
|
109
|
-
The
|
|
222
|
+
The unscoped **`puppertino-table`** package is public on npm. Stable releases use the guarded GitHub Release workflow described in [RELEASING.md](https://github.com/kahwee/puppertino-table/blob/main/RELEASING.md), including verified package preparation and clean registry consumers.
|
|
110
223
|
|
|
111
224
|
```sh
|
|
112
225
|
pnpm check
|
|
113
|
-
npm publish test-results/package/puppertino-table-0.
|
|
226
|
+
npm publish test-results/package/puppertino-table-0.2.0.tgz --dry-run --ignore-scripts --access public
|
|
114
227
|
```
|
|
115
228
|
|
|
116
229
|
`pnpm check` builds and packs the library, checks the tarball file list, and installs it in isolated React 18 and React 19 consumers. `prepack` rebuilds JavaScript and declarations so a locally packed release cannot silently use stale output. Only the compiled library, CSS, package metadata, README, changelog and MIT license ship; Storybook, the demo and tests stay out of the npm package.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export { PuppertinoTable, type PuppertinoTableProps } from './table.js';
|
|
1
|
+
export { PuppertinoTable, type PuppertinoTableProps, type PuppertinoRow, type PuppertinoCell, type PuppertinoRowProps, type PuppertinoCellProps, type PuppertinoDataAttributes, type PuppertinoBodyContext } from './table.js';
|
|
2
2
|
export { createPuppertinoColumnHelper, puppertinoTableFeatures, type PuppertinoTableInstance, type PuppertinoColumnDef } from './model.js';
|
|
3
3
|
export { useColumnResize, type ColumnResizeOptions } from './use-column-resize.js';
|
|
4
|
-
export { ColumnResizeHandle } from './resize-handle.js';
|
|
4
|
+
export { ColumnResizeHandle, type ColumnResizeHandleProps } from './resize-handle.js';
|
|
5
5
|
export { readColumnSizing, columnSizeBounds, clampSize, type ColumnSizeBounds } from './sizing.js';
|
|
6
|
+
export { usePuppertinoTable, type PuppertinoTableOptions } from './use-puppertino-table.js';
|
package/dist/index.js
CHANGED
|
@@ -3,3 +3,4 @@ export { createPuppertinoColumnHelper, puppertinoTableFeatures } from './model.j
|
|
|
3
3
|
export { useColumnResize } from './use-column-resize.js';
|
|
4
4
|
export { ColumnResizeHandle } from './resize-handle.js';
|
|
5
5
|
export { readColumnSizing, columnSizeBounds, clampSize } from './sizing.js';
|
|
6
|
+
export { usePuppertinoTable } from './use-puppertino-table.js';
|
package/dist/resize-handle.d.ts
CHANGED
|
@@ -3,9 +3,11 @@ import type { ColumnResizeOptions } from './use-column-resize.js';
|
|
|
3
3
|
import type { PuppertinoTableInstance } from './model.js';
|
|
4
4
|
import type { Header } from '@tanstack/react-table';
|
|
5
5
|
import { puppertinoTableFeatures } from './model.js';
|
|
6
|
-
export
|
|
6
|
+
export interface ColumnResizeHandleProps<T extends RowData> {
|
|
7
7
|
table: PuppertinoTableInstance<T>;
|
|
8
8
|
header: Header<typeof puppertinoTableFeatures, T>;
|
|
9
|
-
|
|
9
|
+
/** Defaults to a string header or the stable column ID. */
|
|
10
|
+
label?: string;
|
|
10
11
|
options?: ColumnResizeOptions;
|
|
11
|
-
}
|
|
12
|
+
}
|
|
13
|
+
export declare function ColumnResizeHandle<T extends RowData>({ table, header, label, options }: ColumnResizeHandleProps<T>): import("react").JSX.Element | null;
|
package/dist/resize-handle.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useColumnResize } from './use-column-resize.js';
|
|
4
4
|
export function ColumnResizeHandle({ table, header, label, options }) {
|
|
5
|
+
const resolvedLabel = label ?? (typeof header.column.columnDef.header === 'string' ? header.column.columnDef.header : header.column.id);
|
|
5
6
|
const { handle } = useColumnResize(table, options);
|
|
6
7
|
if (options?.enabled === false || header.isPlaceholder || header.column.columns.length > 0)
|
|
7
8
|
return null;
|
|
8
|
-
return _jsx("span", { className: "pt-resizer", role: "separator", tabIndex: 0, "aria-orientation": "vertical", "aria-label": `Resize ${
|
|
9
|
+
return _jsx("span", { className: "pt-resizer", role: "separator", tabIndex: 0, "aria-orientation": "vertical", "aria-label": `Resize ${resolvedLabel} column`, "aria-valuemin": header.column.columnDef.minSize ?? 20, "aria-valuemax": header.column.columnDef.maxSize ?? 1200, "aria-valuenow": header.getSize(), "aria-valuetext": `${header.getSize()} pixels`, title: "Drag to resize. Double-click or Enter to reset. Arrow keys adjust width; Escape cancels a drag.", ...handle(header.column.id) });
|
|
9
10
|
}
|
package/dist/table.d.ts
CHANGED
|
@@ -1,27 +1,32 @@
|
|
|
1
|
-
import type { RowData } from '@tanstack/react-table';
|
|
2
|
-
import { type ReactNode } from 'react';
|
|
3
|
-
import {
|
|
4
|
-
import { type
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
import type { RowData, Row, Cell } from '@tanstack/react-table';
|
|
2
|
+
import { type ReactNode, type ComponentPropsWithoutRef } from 'react';
|
|
3
|
+
import { puppertinoTableFeatures, type PuppertinoTableInstance } from './model.js';
|
|
4
|
+
import { type PuppertinoTableOptions } from './use-puppertino-table.js';
|
|
5
|
+
export type PuppertinoRow<T extends RowData> = Row<typeof puppertinoTableFeatures, T>;
|
|
6
|
+
export type PuppertinoCell<T extends RowData> = Cell<typeof puppertinoTableFeatures, T>;
|
|
7
|
+
export type PuppertinoDataAttributes = {
|
|
8
|
+
[key: `data-${string}`]: string | number | boolean | null | undefined;
|
|
9
|
+
};
|
|
10
|
+
export interface PuppertinoBodyContext<T extends RowData> {
|
|
11
|
+
table: PuppertinoTableInstance<T>;
|
|
12
|
+
/** Leaf columns plus the optional selection column. */
|
|
13
|
+
columnCount: number;
|
|
14
|
+
selectable: boolean;
|
|
15
|
+
disabled: boolean;
|
|
16
|
+
}
|
|
17
|
+
export type PuppertinoRowProps = PuppertinoDataAttributes & Omit<ComponentPropsWithoutRef<'tr'>, 'children' | 'dangerouslySetInnerHTML'>;
|
|
18
|
+
export type PuppertinoCellProps = PuppertinoDataAttributes & Omit<ComponentPropsWithoutRef<'td'>, 'children' | 'dangerouslySetInnerHTML'>;
|
|
19
|
+
export interface PuppertinoTableProps<T extends RowData> extends PuppertinoTableOptions<T> {
|
|
15
20
|
getRowLabel?: (row: T) => string;
|
|
16
|
-
selectable?: boolean;
|
|
17
|
-
disabled?: boolean;
|
|
18
21
|
resizable?: boolean;
|
|
19
|
-
storageKey?: string;
|
|
20
|
-
storage?: Pick<Storage, 'getItem' | 'setItem'>;
|
|
21
22
|
cancelMediaQuery?: string;
|
|
22
23
|
ariaLabel: string;
|
|
23
24
|
caption?: ReactNode;
|
|
24
25
|
emptyContent?: ReactNode;
|
|
25
26
|
className?: string;
|
|
27
|
+
getRowProps?: (row: PuppertinoRow<T>, context: PuppertinoBodyContext<T>) => PuppertinoRowProps | undefined;
|
|
28
|
+
getCellProps?: (cell: PuppertinoCell<T>, context: PuppertinoBodyContext<T>) => PuppertinoCellProps | undefined;
|
|
29
|
+
/** Render semantic <tr> elements before data rows, including when data is empty. */
|
|
30
|
+
leadingRows?: ReactNode | ((table: PuppertinoTableInstance<T>, context: PuppertinoBodyContext<T>) => ReactNode);
|
|
26
31
|
}
|
|
27
|
-
export declare function PuppertinoTable<T extends RowData>({ data, columns, getRowId, sorting, onSortingChange, manualSorting, rowSelection: controlledSelection, onRowSelectionChange, getRowLabel, selectable, disabled, resizable, storageKey, storage, cancelMediaQuery, ariaLabel, caption, emptyContent, className }: PuppertinoTableProps<T>): import("react").JSX.Element;
|
|
32
|
+
export declare function PuppertinoTable<T extends RowData>({ data, columns, getRowId, sorting, onSortingChange, initialSorting, initialRowSelection, manualSorting, rowSelection: controlledSelection, onRowSelectionChange, getRowLabel, selectable, disabled, resizable, storageKey, storage, cancelMediaQuery, ariaLabel, caption, emptyContent, className, getRowProps, getCellProps, leadingRows }: PuppertinoTableProps<T>): import("react").JSX.Element;
|
package/dist/table.js
CHANGED
|
@@ -1,38 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { createElement as _createElement } from "react";
|
|
2
3
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { memo
|
|
4
|
-
import {
|
|
5
|
-
import { puppertinoTableFeatures } from './model.js';
|
|
6
|
-
import { columnSizeBounds, readColumnSizing } from './sizing.js';
|
|
4
|
+
import { memo } from 'react';
|
|
5
|
+
import { usePuppertinoTable } from './use-puppertino-table.js';
|
|
7
6
|
import { useColumnResize } from './use-column-resize.js';
|
|
8
|
-
export function PuppertinoTable({ data, columns, getRowId, sorting, onSortingChange, manualSorting = true, rowSelection: controlledSelection, onRowSelectionChange, getRowLabel, selectable = false, disabled = false, resizable = true, storageKey, storage, cancelMediaQuery, ariaLabel, caption, emptyContent = 'No rows', className = '' }) {
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
const canSelect = selectable && !disabled && (controlledSelection === undefined || !!onRowSelectionChange);
|
|
12
|
-
const bounds = useMemo(() => columnSizeBounds(columns), [columns]);
|
|
13
|
-
const sizedColumns = useMemo(() => {
|
|
14
|
-
const byId = new Map(bounds.map(bound => [bound.id, bound]));
|
|
15
|
-
const decorate = (defs) => defs.map(def => {
|
|
16
|
-
if ('columns' in def && def.columns)
|
|
17
|
-
return { ...def, columns: decorate(def.columns) };
|
|
18
|
-
const id = def.id ?? ('accessorKey' in def ? String(def.accessorKey).replaceAll('.', '_') : '');
|
|
19
|
-
return { ...def, ...byId.get(id) };
|
|
20
|
-
});
|
|
21
|
-
return decorate(columns);
|
|
22
|
-
}, [columns, bounds]);
|
|
23
|
-
const [initialWidths] = useState(() => {
|
|
24
|
-
try {
|
|
25
|
-
return readColumnSizing(bounds, storageKey, storage || localStorage);
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
return readColumnSizing(bounds);
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
const table = useTable({ features: puppertinoTableFeatures, columns: sizedColumns, data, getRowId,
|
|
32
|
-
manualSorting, enableMultiSort: false, enableSortingRemoval: false, enableRowSelection: canSelect,
|
|
33
|
-
enableSubRowSelection: false, initialState: { columnSizing: initialWidths },
|
|
34
|
-
state: { sorting, rowSelection }, onSortingChange, onRowSelectionChange: onRowSelectionChange ?? setLocalSelection,
|
|
35
|
-
}, state => ({ sorting: state.sorting, rowSelection: state.rowSelection }));
|
|
7
|
+
export function PuppertinoTable({ data, columns, getRowId, sorting, onSortingChange, initialSorting, initialRowSelection, manualSorting = true, rowSelection: controlledSelection, onRowSelectionChange, getRowLabel, selectable = false, disabled = false, resizable = true, storageKey, storage, cancelMediaQuery, ariaLabel, caption, emptyContent = 'No rows', className = '', getRowProps, getCellProps, leadingRows }) {
|
|
8
|
+
const table = usePuppertinoTable({ data, columns, getRowId, sorting, onSortingChange, initialSorting, initialRowSelection, manualSorting,
|
|
9
|
+
rowSelection: controlledSelection, onRowSelectionChange, selectable, disabled, storageKey, storage });
|
|
36
10
|
const options = { enabled: resizable && !disabled, storageKey, storage, cancelMediaQuery };
|
|
37
11
|
const { handle } = useColumnResize(table, options);
|
|
38
12
|
const groups = table.getHeaderGroups();
|
|
@@ -42,11 +16,19 @@ export function PuppertinoTable({ data, columns, getRowId, sorting, onSortingCha
|
|
|
42
16
|
const sorted = column.getIsSorted();
|
|
43
17
|
const leaf = column.columns.length === 0;
|
|
44
18
|
return _jsxs("th", { colSpan: header.colSpan, scope: leaf ? 'col' : 'colgroup', "aria-sort": sorted ? sorted === 'asc' ? 'ascending' : 'descending' : undefined, children: [!header.isPlaceholder && (column.getCanSort() ? _jsxs("button", { type: "button", className: "pt-sort", "aria-label": `Sort by ${label}`, disabled: disabled, onClick: () => column.toggleSorting(), children: [_jsx(table.FlexRender, { header: header }), _jsx("span", { className: "pt-sort-arrow", "aria-hidden": "true", "data-direction": sorted || 'none', children: "\u2304" })] }) : _jsx(table.FlexRender, { header: header })), leaf && !header.isPlaceholder && resizable && !disabled && _jsx("span", { className: "pt-resizer", role: "separator", tabIndex: 0, "aria-orientation": "vertical", "aria-label": `Resize ${label} column`, "aria-valuemin": column.columnDef.minSize, "aria-valuemax": column.columnDef.maxSize, "aria-valuenow": header.getSize(), "aria-valuetext": `${header.getSize()} pixels`, title: "Drag to resize. Double-click or Enter to reset. Arrows adjust width; Escape cancels a drag.", ...handle(column.id) })] }, header.id);
|
|
45
|
-
})] }, group.id)) }), _jsx(MemoTableBody, { table: table, selectable: selectable, getRowLabel: getRowLabel, emptyContent: emptyContent })] }) }) });
|
|
19
|
+
})] }, group.id)) }), _jsx(MemoTableBody, { table: table, selectable: selectable, disabled: disabled, getRowLabel: getRowLabel, emptyContent: emptyContent, getRowProps: getRowProps, getCellProps: getCellProps, leadingRows: leadingRows })] }) }) });
|
|
46
20
|
}
|
|
47
21
|
/** Geometry updates do not rerender cells. State and data changes still do. */
|
|
48
|
-
function TableBody({ table, selectable, getRowLabel, emptyContent }) {
|
|
22
|
+
function TableBody({ table, selectable, disabled, getRowLabel, emptyContent, getRowProps, getCellProps, leadingRows }) {
|
|
49
23
|
const rows = table.getRowModel().rows;
|
|
50
|
-
|
|
24
|
+
const context = { table, selectable, disabled, columnCount: table.getAllLeafColumns().length + (selectable ? 1 : 0) };
|
|
25
|
+
return _jsxs("tbody", { children: [typeof leadingRows === 'function' ? leadingRows(table, context) : leadingRows, rows.length ? rows.map(row => {
|
|
26
|
+
const props = getRowProps?.(row, context);
|
|
27
|
+
const className = [row.getIsSelected() ? 'p-selected' : '', props?.className].filter(Boolean).join(' ') || undefined;
|
|
28
|
+
return _createElement("tr", { ...props, key: row.id, className: className },
|
|
29
|
+
selectable && _jsx("td", { className: "pt-selection", children: _jsx("input", { type: "checkbox", "aria-label": `Select ${getRowLabel?.(row.original) ?? row.id}`, checked: row.getIsSelected(), disabled: !row.getCanSelect(), onClick: event => { event.stopPropagation(); row.getToggleSelectedHandler()(event); }, onChange: () => { } }) }),
|
|
30
|
+
row.getAllCells().map(cell => _createElement("td", { ...getCellProps?.(cell, context), key: cell.id },
|
|
31
|
+
_jsx(table.FlexRender, { cell: cell }))));
|
|
32
|
+
}) : _jsx("tr", { children: _jsx("td", { colSpan: context.columnCount, children: emptyContent }) })] });
|
|
51
33
|
}
|
|
52
34
|
const MemoTableBody = memo(TableBody);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { RowData } from '@tanstack/react-table';
|
|
2
2
|
import { type PointerEvent, type KeyboardEvent } from 'react';
|
|
3
|
-
import type { PuppertinoTableInstance } from
|
|
3
|
+
import type { PuppertinoTableInstance } from './model.js';
|
|
4
4
|
export interface ColumnResizeOptions {
|
|
5
5
|
enabled?: boolean;
|
|
6
6
|
storageKey?: string;
|
|
@@ -8,7 +8,7 @@ export interface ColumnResizeOptions {
|
|
|
8
8
|
cancelMediaQuery?: string;
|
|
9
9
|
dragClassName?: string;
|
|
10
10
|
}
|
|
11
|
-
export declare function useColumnResize<T extends RowData>(table: PuppertinoTableInstance<T>,
|
|
11
|
+
export declare function useColumnResize<T extends RowData>(table: PuppertinoTableInstance<T>, options?: ColumnResizeOptions): {
|
|
12
12
|
handle: (id: string) => {
|
|
13
13
|
onPointerDown: (event: PointerEvent<HTMLElement>) => void;
|
|
14
14
|
onPointerMove: (event: PointerEvent<HTMLElement>) => void;
|
|
@@ -10,106 +10,140 @@ function markDrag(name, delta) {
|
|
|
10
10
|
activeClasses.delete(name);
|
|
11
11
|
document.documentElement.classList.toggle(name, count > 0);
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
/** One gesture and one set of global listeners for each table's sizing store. */
|
|
14
|
+
function createController() {
|
|
15
|
+
const members = new Set();
|
|
16
|
+
let drag = null;
|
|
17
|
+
const snapshot = (table) => Object.fromEntries(table.getAllLeafColumns().map(column => [column.id, column.getSize()]));
|
|
18
|
+
const bounds = (table, id) => {
|
|
15
19
|
const column = table.getColumn(id);
|
|
16
20
|
return { id, size: column.columnDef.size ?? 150, minSize: column.columnDef.minSize ?? 20, maxSize: column.columnDef.maxSize ?? Number.MAX_SAFE_INTEGER };
|
|
17
21
|
};
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
catch { /* Resizing still works without storage. */ } };
|
|
25
|
-
const reset = (id) => { table.getColumn(id).resetSize(); save(); };
|
|
26
|
-
const drag = useRef(null);
|
|
27
|
-
const apply = (next, persist = false) => {
|
|
28
|
-
table.setColumnSizing(next);
|
|
29
|
-
if (persist)
|
|
30
|
-
save();
|
|
22
|
+
const save = ({ table, options }) => {
|
|
23
|
+
try {
|
|
24
|
+
if (options.storageKey)
|
|
25
|
+
(options.storage || localStorage).setItem(options.storageKey, JSON.stringify(snapshot(table)));
|
|
26
|
+
}
|
|
27
|
+
catch { /* Storage is optional. */ }
|
|
31
28
|
};
|
|
32
29
|
const finish = (cancel = false) => {
|
|
33
|
-
const active = drag
|
|
30
|
+
const active = drag;
|
|
34
31
|
if (!active)
|
|
35
32
|
return;
|
|
36
|
-
drag
|
|
37
|
-
|
|
33
|
+
drag = null;
|
|
34
|
+
active.removeMedia();
|
|
35
|
+
markDrag(active.className, -1);
|
|
38
36
|
if (active.target.hasPointerCapture(active.pointer))
|
|
39
37
|
active.target.releasePointerCapture(active.pointer);
|
|
40
|
-
|
|
38
|
+
active.member.table.setColumnSizing(cancel ? active.before : snapshot(active.member.table));
|
|
39
|
+
if (!cancel)
|
|
40
|
+
save(active.member);
|
|
41
41
|
};
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
event.stopPropagation();
|
|
77
|
-
event.currentTarget.focus();
|
|
78
|
-
const before = snapshot();
|
|
79
|
-
drag.current = { id, x: event.clientX, width: before[id], before, target: event.currentTarget, pointer: event.pointerId };
|
|
80
|
-
event.currentTarget.setPointerCapture(event.pointerId);
|
|
81
|
-
markDrag(dragClassName, 1);
|
|
82
|
-
},
|
|
83
|
-
onPointerMove: (event) => {
|
|
84
|
-
const active = drag.current;
|
|
85
|
-
if (active && active.pointer === event.pointerId)
|
|
86
|
-
apply({ ...snapshot(), [active.id]: clamp(active.id, active.width + event.clientX - active.x) });
|
|
87
|
-
},
|
|
88
|
-
onPointerUp: (event) => { if (event.pointerId === drag.current?.pointer)
|
|
89
|
-
finish(!enabled || !!(cancelMediaQuery && window.matchMedia(cancelMediaQuery).matches)); },
|
|
90
|
-
onPointerCancel: (event) => { if (event.pointerId === drag.current?.pointer)
|
|
91
|
-
finish(true); },
|
|
92
|
-
onLostPointerCapture: (event) => { if (event.pointerId === drag.current?.pointer)
|
|
93
|
-
finish(true); },
|
|
94
|
-
onDoubleClick: () => { if (enabled)
|
|
95
|
-
reset(id); },
|
|
96
|
-
onKeyDown: (event) => {
|
|
97
|
-
if (!enabled || drag.current)
|
|
98
|
-
return;
|
|
99
|
-
if (event.key === "Enter") {
|
|
42
|
+
const cancel = (event) => { if (event.key === 'Escape' && drag) {
|
|
43
|
+
event.preventDefault();
|
|
44
|
+
finish(true);
|
|
45
|
+
} };
|
|
46
|
+
const blur = () => finish(true);
|
|
47
|
+
const cancelMember = (member) => { if (drag?.member === member)
|
|
48
|
+
finish(true); };
|
|
49
|
+
return {
|
|
50
|
+
cancelMember,
|
|
51
|
+
retain(member) {
|
|
52
|
+
if (!members.size) {
|
|
53
|
+
window.addEventListener('keydown', cancel);
|
|
54
|
+
window.addEventListener('blur', blur);
|
|
55
|
+
}
|
|
56
|
+
members.add(member);
|
|
57
|
+
return () => {
|
|
58
|
+
cancelMember(member);
|
|
59
|
+
members.delete(member);
|
|
60
|
+
if (!members.size) {
|
|
61
|
+
window.removeEventListener('keydown', cancel);
|
|
62
|
+
window.removeEventListener('blur', blur);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
handle(id, member) {
|
|
67
|
+
const reset = () => { member.table.getColumn(id).resetSize(); save(member); };
|
|
68
|
+
return {
|
|
69
|
+
onPointerDown: (event) => {
|
|
70
|
+
const { table, options } = member;
|
|
71
|
+
if (options.enabled === false || event.button !== 0 || drag || !members.has(member))
|
|
72
|
+
return;
|
|
73
|
+
const media = options.cancelMediaQuery ? window.matchMedia(options.cancelMediaQuery) : undefined;
|
|
74
|
+
if (media?.matches)
|
|
75
|
+
return;
|
|
100
76
|
event.preventDefault();
|
|
101
77
|
event.stopPropagation();
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
78
|
+
event.currentTarget.focus();
|
|
79
|
+
const before = snapshot(table);
|
|
80
|
+
const modeChange = () => { if (media?.matches)
|
|
81
|
+
finish(true); };
|
|
82
|
+
drag = { member, id, x: event.clientX, width: before[id], before, target: event.currentTarget,
|
|
83
|
+
pointer: event.pointerId, className: options.dragClassName ?? 'pt-resizing', media,
|
|
84
|
+
removeMedia: () => media?.removeEventListener('change', modeChange) };
|
|
85
|
+
media?.addEventListener('change', modeChange);
|
|
86
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
87
|
+
markDrag(drag.className, 1);
|
|
88
|
+
},
|
|
89
|
+
onPointerMove: (event) => {
|
|
90
|
+
const active = drag;
|
|
91
|
+
if (active?.member === member && active.pointer === event.pointerId) {
|
|
92
|
+
const { table } = member;
|
|
93
|
+
table.setColumnSizing({ ...snapshot(table), [active.id]: clampSize(active.width + event.clientX - active.x, bounds(table, active.id)) });
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
onPointerUp: (event) => {
|
|
97
|
+
if (drag?.member === member && event.pointerId === drag.pointer)
|
|
98
|
+
finish(member.options.enabled === false || !!drag.media?.matches);
|
|
99
|
+
},
|
|
100
|
+
onPointerCancel: (event) => { if (drag?.member === member && event.pointerId === drag.pointer)
|
|
101
|
+
finish(true); },
|
|
102
|
+
onLostPointerCapture: (event) => { if (drag?.member === member && event.pointerId === drag.pointer)
|
|
103
|
+
finish(true); },
|
|
104
|
+
onDoubleClick: () => { if (member.options.enabled !== false && !drag)
|
|
105
|
+
reset(); },
|
|
106
|
+
onKeyDown: (event) => {
|
|
107
|
+
if (member.options.enabled === false || drag)
|
|
108
|
+
return;
|
|
109
|
+
if (event.key === 'Enter') {
|
|
110
|
+
event.preventDefault();
|
|
111
|
+
event.stopPropagation();
|
|
112
|
+
reset();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const { table } = member;
|
|
116
|
+
const step = event.shiftKey ? 40 : 16;
|
|
117
|
+
const widths = snapshot(table);
|
|
118
|
+
const next = event.key === 'ArrowLeft' ? widths[id] - step : event.key === 'ArrowRight' ? widths[id] + step : event.key === 'Home' ? bounds(table, id).minSize : event.key === 'End' ? bounds(table, id).maxSize : undefined;
|
|
119
|
+
if (next === undefined)
|
|
120
|
+
return;
|
|
121
|
+
event.preventDefault();
|
|
122
|
+
event.stopPropagation();
|
|
123
|
+
table.setColumnSizing({ ...widths, [id]: clampSize(next, bounds(table, id)) });
|
|
124
|
+
save(member);
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// The atom survives table wrapper rerenders. Weak keys allow discarded tables to be collected.
|
|
131
|
+
const controllers = new WeakMap();
|
|
132
|
+
export function useColumnResize(table, options = {}) {
|
|
133
|
+
const key = table.atoms.columnSizing;
|
|
134
|
+
let controller = controllers.get(key);
|
|
135
|
+
if (!controller) {
|
|
136
|
+
controller = createController();
|
|
137
|
+
controllers.set(key, controller);
|
|
138
|
+
}
|
|
139
|
+
const member = useRef({ table, options });
|
|
140
|
+
member.current.table = table;
|
|
141
|
+
member.current.options = options;
|
|
142
|
+
useEffect(() => controller.retain(member.current), [controller]);
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
if (options.enabled === false)
|
|
145
|
+
controller.cancelMember(member.current);
|
|
146
|
+
return () => controller.cancelMember(member.current);
|
|
147
|
+
}, [controller, options.enabled, options.storageKey, options.storage, options.cancelMediaQuery, options.dragClassName]);
|
|
148
|
+
return { handle: (id) => controller.handle(id, member.current) };
|
|
115
149
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { RowData, SortingState, RowSelectionState, OnChangeFn } from '@tanstack/react-table';
|
|
2
|
+
import { type PuppertinoColumnDef, type PuppertinoTableInstance } from './model.js';
|
|
3
|
+
export interface PuppertinoTableOptions<T extends RowData> {
|
|
4
|
+
data: readonly T[];
|
|
5
|
+
columns: readonly PuppertinoColumnDef<T>[];
|
|
6
|
+
getRowId: (row: T, index: number) => string;
|
|
7
|
+
/** Omit for local state. Supply a callback alongside controlled sorting to allow edits. */
|
|
8
|
+
sorting?: SortingState;
|
|
9
|
+
onSortingChange?: OnChangeFn<SortingState>;
|
|
10
|
+
initialSorting?: SortingState;
|
|
11
|
+
initialRowSelection?: RowSelectionState;
|
|
12
|
+
/** Preserve host-owned order by default, including server-paginated data. */
|
|
13
|
+
manualSorting?: boolean;
|
|
14
|
+
rowSelection?: RowSelectionState;
|
|
15
|
+
onRowSelectionChange?: OnChangeFn<RowSelectionState>;
|
|
16
|
+
selectable?: boolean;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
storageKey?: string;
|
|
19
|
+
storage?: Pick<Storage, 'getItem' | 'setItem'>;
|
|
20
|
+
}
|
|
21
|
+
/** Shared state wiring for complete tables and custom semantic layouts. */
|
|
22
|
+
export declare function usePuppertinoTable<T extends RowData>({ data, columns, getRowId, sorting: controlledSorting, onSortingChange, initialSorting, initialRowSelection, manualSorting, rowSelection: controlledSelection, onRowSelectionChange, selectable, disabled, storageKey, storage }: PuppertinoTableOptions<T>): PuppertinoTableInstance<T>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useTable } from '@tanstack/react-table';
|
|
3
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
4
|
+
import { puppertinoTableFeatures } from './model.js';
|
|
5
|
+
import { columnSizeBounds, readColumnSizing } from './sizing.js';
|
|
6
|
+
/** Shared state wiring for complete tables and custom semantic layouts. */
|
|
7
|
+
export function usePuppertinoTable({ data, columns, getRowId, sorting: controlledSorting, onSortingChange, initialSorting = [], initialRowSelection = {}, manualSorting = true, rowSelection: controlledSelection, onRowSelectionChange, selectable = false, disabled = false, storageKey, storage }) {
|
|
8
|
+
const [localSelection, setLocalSelection] = useState(() => initialRowSelection);
|
|
9
|
+
const [localSorting, setLocalSorting] = useState(() => initialSorting);
|
|
10
|
+
const sorting = controlledSorting ?? localSorting;
|
|
11
|
+
const changeSorting = useCallback(update => {
|
|
12
|
+
if (controlledSorting === undefined)
|
|
13
|
+
setLocalSorting(update);
|
|
14
|
+
onSortingChange?.(update);
|
|
15
|
+
}, [controlledSorting, onSortingChange]);
|
|
16
|
+
const changeSelection = useCallback(update => {
|
|
17
|
+
if (controlledSelection === undefined)
|
|
18
|
+
setLocalSelection(update);
|
|
19
|
+
onRowSelectionChange?.(update);
|
|
20
|
+
}, [controlledSelection, onRowSelectionChange]);
|
|
21
|
+
const rowSelection = controlledSelection ?? localSelection;
|
|
22
|
+
const canSelect = selectable && !disabled && (controlledSelection === undefined || !!onRowSelectionChange);
|
|
23
|
+
const bounds = useMemo(() => columnSizeBounds(columns), [columns]);
|
|
24
|
+
const sizedColumns = useMemo(() => {
|
|
25
|
+
const byId = new Map(bounds.map(bound => [bound.id, bound]));
|
|
26
|
+
const decorate = (defs) => defs.map(def => {
|
|
27
|
+
if ('columns' in def && def.columns)
|
|
28
|
+
return { ...def, columns: decorate(def.columns) };
|
|
29
|
+
const id = def.id ?? ('accessorKey' in def ? String(def.accessorKey).replaceAll('.', '_') : '');
|
|
30
|
+
return { ...def, ...byId.get(id) };
|
|
31
|
+
});
|
|
32
|
+
return decorate(columns);
|
|
33
|
+
}, [columns, bounds]);
|
|
34
|
+
const [initialWidths] = useState(() => {
|
|
35
|
+
try {
|
|
36
|
+
return readColumnSizing(bounds, storageKey, storage || localStorage);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return readColumnSizing(bounds);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
const table = useTable({ features: puppertinoTableFeatures, columns: sizedColumns, data, getRowId,
|
|
43
|
+
manualSorting, enableSorting: controlledSorting === undefined || !!onSortingChange, enableMultiSort: false, enableSortingRemoval: false, enableRowSelection: canSelect,
|
|
44
|
+
enableSubRowSelection: false, initialState: { columnSizing: initialWidths },
|
|
45
|
+
state: { sorting, rowSelection }, onSortingChange: changeSorting, onRowSelectionChange: changeSelection,
|
|
46
|
+
}, state => ({ sorting: state.sorting, rowSelection: state.rowSelection }));
|
|
47
|
+
return table;
|
|
48
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "puppertino-table",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"build": "node scripts/build.mjs",
|
|
35
35
|
"build:demo": "vite build",
|
|
36
36
|
"typecheck": "tsc --noEmit",
|
|
37
|
-
"test": "tsx --test tests/*.test.ts",
|
|
37
|
+
"test": "tsx --test tests/*.test.ts && node --test tests/release.test.mjs",
|
|
38
38
|
"test:browser": "tsx scripts/check-browser.ts",
|
|
39
39
|
"check": "pnpm typecheck && pnpm test && pnpm build && pnpm build:demo && pnpm build:storybook && pnpm test:browser && pnpm test:package",
|
|
40
40
|
"storybook": "storybook dev -p 6006 --no-open",
|