@xeplr/ui-table 1.0.0 → 1.0.2
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/README.md +151 -0
- package/package.json +21 -5
- package/src/CellZoom.jsx +77 -0
- package/src/XeplrTable.jsx +349 -25
- package/src/actions/ActionsCell.jsx +36 -1
- package/src/columnWidths.js +333 -0
- package/src/filters/FilterWrapper.jsx +79 -30
- package/src/index.js +18 -0
- package/src/renderers/_helpers.js +43 -0
- package/src/renderers/avatarName.jsx +31 -0
- package/src/renderers/currency.jsx +25 -0
- package/src/renderers/dateDisplay.jsx +38 -0
- package/src/renderers/index.js +22 -0
- package/src/renderers/link.jsx +24 -0
- package/src/renderers/memberChips.jsx +35 -0
- package/src/renderers/statusBadge.jsx +15 -0
- package/src/renderers/tags.jsx +23 -0
- package/src/renderers/twoLine.jsx +16 -0
- package/src/tableStyles.js +236 -0
- package/src/useColumnWidths.js +280 -0
- package/src/useTableController.js +74 -7
- package/src/xeplr-table.css +399 -253
package/README.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# @xeplr/ui-table
|
|
2
|
+
|
|
3
|
+
Schema-driven React data table. Sort, filter, paginate, edit, nested children, transactional commits, config-driven cell renderers. Gold-on-dark theme.
|
|
4
|
+
|
|
5
|
+
(The package name on npm is `@xeplr/ui-table` — the GitHub repo and folder are named `xeplr-ui-table`.)
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm i @xeplr/ui-table
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Peer dep: `react ^18 || ^19`.
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```jsx
|
|
18
|
+
import { XeplrTable } from '@xeplr/ui-table';
|
|
19
|
+
|
|
20
|
+
<XeplrTable
|
|
21
|
+
data={projects}
|
|
22
|
+
schema={{ 0: { key: 'projects', columns: [
|
|
23
|
+
{ accessor: 'name', header: 'Project', render: { type: 'twoLine', subKey: 'subtitle' } },
|
|
24
|
+
{ accessor: 'client', header: 'Client', render: { type: 'avatarName', subKey: 'clientType' } },
|
|
25
|
+
{ accessor: 'tags', header: 'Tags', render: { type: 'tags' } },
|
|
26
|
+
{ accessor: 'budget', header: 'Budget', render: { type: 'currency', currency: 'INR' } },
|
|
27
|
+
{ accessor: 'members', header: 'Team', render: { type: 'memberChips', max: 3 } }
|
|
28
|
+
]}}}
|
|
29
|
+
onCommit={async (changeSet) => api.save(changeSet)}
|
|
30
|
+
/>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Filters, sort, pagination, type detection are all automatic.
|
|
34
|
+
|
|
35
|
+
## Column config
|
|
36
|
+
|
|
37
|
+
Each column object:
|
|
38
|
+
|
|
39
|
+
| field | type | notes |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `accessor` | string | required — row property |
|
|
42
|
+
| `header` | string | column title |
|
|
43
|
+
| `dataType` | `'string'\|'number'\|'date'\|'boolean'` | optional — auto-detected from data |
|
|
44
|
+
| `render` | `{ type, ...config }` or string | declarative cell — see renderers |
|
|
45
|
+
| `cell` | `(ctx) => ReactNode` | escape hatch — custom JSX (overrides `render`) |
|
|
46
|
+
| `cellStyle` | array | conditional styles — `[{ when: '$.status is active', backgroundColor, color }]` |
|
|
47
|
+
| `enableSorting` | boolean | default true |
|
|
48
|
+
| `enableColumnFilter` | boolean | default true |
|
|
49
|
+
|
|
50
|
+
## Renderers
|
|
51
|
+
|
|
52
|
+
Pass via `render: { type: 'X', ...config }`. String shorthand `render: 'X'` works for renderers with no config.
|
|
53
|
+
|
|
54
|
+
| type | config | value shape | output |
|
|
55
|
+
|---|---|---|---|
|
|
56
|
+
| `avatarName` | `{ subKey?, size? }` | string | avatar+bold name [+ subtitle from `row[subKey]`] |
|
|
57
|
+
| `twoLine` | `{ subKey? }` | string | primary line [+ subtitle from `row[subKey]`] |
|
|
58
|
+
| `tags` | `{ variant?: 'gold'\|'muted'\|'blue', max? }` | `string[]` or comma-string | pill list, `+N` overflow |
|
|
59
|
+
| `currency` | `{ currency?: 'INR', position?: 'before', decimals?: 0, bold? }` | number | symbol + formatted number |
|
|
60
|
+
| `memberChips` | `{ max?: 3, nameKey?: 'name' }` | `Array<{name,...}>` | avatar+name chips, `+N` overflow |
|
|
61
|
+
| `statusBadge` | `{ map: { value: { bg, color, label? } }, default? }` | string | colored pill |
|
|
62
|
+
| `dateDisplay` | `{ format?: 'date'\|'datetime'\|'relative' }` | Date or ISO string | formatted date |
|
|
63
|
+
| `link` | `{ onClick?(row), hrefKey?, target? }` | string | clickable text or `<a>` |
|
|
64
|
+
|
|
65
|
+
Add your own: drop a file in `src/renderers/<name>.jsx` exporting `default function (config) { return cell; }`, register it in `renderers/index.js`. Or import the registry and extend at runtime: `import { renderers } from '@xeplr/ui-table'`.
|
|
66
|
+
|
|
67
|
+
## Conditional cell styling
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
{ accessor: 'status', cellStyle: [
|
|
71
|
+
{ when: '$.status is active', backgroundColor: '#1b5e20', color: '#a5d6a7' },
|
|
72
|
+
{ when: '$.budget < 100000', backgroundColor: '#bf360c', color: '#ffab91' }
|
|
73
|
+
]}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Operators: `is`, `is not`, `>`, `<`, `>=`, `<=`, `contains`, `starts with`, `ends with`, `is empty`, `is not empty`. See `operators.js`.
|
|
77
|
+
|
|
78
|
+
## Nested data (schema levels)
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
schema = {
|
|
82
|
+
0: { key: 'teams', columns: [...] }, // root
|
|
83
|
+
1: { key: 'employees', columns: [...] }, // children of each row
|
|
84
|
+
2: { key: 'tasks', columns: [...] } // grandchildren
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Each row at level N must have `row[schema[N+1].key]` as an array.
|
|
89
|
+
|
|
90
|
+
`childDisplay` prop:
|
|
91
|
+
- `'popup'` (default) — double-click row → modal with children
|
|
92
|
+
- `'inner'` — expand arrow inline
|
|
93
|
+
|
|
94
|
+
## Edit / add / delete / commit
|
|
95
|
+
|
|
96
|
+
Pass `onCommit={async (changeSet) => ...}`. Toolbar gets **+ Add New**, row checkboxes get **Delete Selected**, double-click → edit modal. Edits stage locally; **Commit** flushes a deep diff:
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
[
|
|
100
|
+
{ op: 'add', path: ['teams', null], record: {...} },
|
|
101
|
+
{ op: 'update', path: ['teams', '3'], record: {...}, changes: { status: 'inactive' } },
|
|
102
|
+
{ op: 'delete', path: ['teams', '5', 'employees', 'e7'] }
|
|
103
|
+
]
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`buildChangeSet(originalData, stagedData, schema)` is exported if you want to compute diffs yourself.
|
|
107
|
+
|
|
108
|
+
## Props (XeplrTable)
|
|
109
|
+
|
|
110
|
+
| prop | type | default |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| `data` | `Array<object>` | required |
|
|
113
|
+
| `schema` | `{ 0: { key, columns }, 1?, 2?, ... }` | required |
|
|
114
|
+
| `childDisplay` | `'popup'\|'inner'` | `'popup'` |
|
|
115
|
+
| `pageSize` | number | 20 |
|
|
116
|
+
| `enableSorting` | boolean | true |
|
|
117
|
+
| `enableFiltering` | boolean | true |
|
|
118
|
+
| `enablePagination` | boolean | true |
|
|
119
|
+
| `onCommit` | `async (changeSet) => void` | — (omit to make table read-only) |
|
|
120
|
+
| `className` | string | — |
|
|
121
|
+
|
|
122
|
+
## Theme
|
|
123
|
+
|
|
124
|
+
Gold-on-dark by default. Override in your stylesheet — every chrome rule and renderer class is namespaced `.xeplr-table-*` / `.xeplr-r-*`. The override block at the bottom of `xeplr-table.css` is the canonical theme definition.
|
|
125
|
+
|
|
126
|
+
## Files
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
src/
|
|
130
|
+
XeplrTable.jsx ─ main component
|
|
131
|
+
useTableController.js ─ TanStack wiring + renderer dispatch
|
|
132
|
+
useActionsController.js ─ add/edit/delete staging + commit
|
|
133
|
+
detectTypes.js ─ infers column dataType from rows
|
|
134
|
+
resolveCellStyle.js ─ evaluates `cellStyle` rules
|
|
135
|
+
operators.js ─ shared comparison logic
|
|
136
|
+
buildChangeSet.js ─ deep-diff for transactional commits
|
|
137
|
+
xeplr-table.css ─ chrome + renderer styles + theme
|
|
138
|
+
|
|
139
|
+
filters/
|
|
140
|
+
StringFilter.jsx · NumberFilter.jsx · DateFilter.jsx · BooleanFilter.jsx · FilterWrapper.jsx
|
|
141
|
+
actions/
|
|
142
|
+
ActionsCell.jsx · RecordModal.jsx · RecordDetail.jsx · ChildTable.jsx
|
|
143
|
+
renderers/
|
|
144
|
+
avatarName.jsx · twoLine.jsx · tags.jsx · currency.jsx
|
|
145
|
+
memberChips.jsx · statusBadge.jsx · dateDisplay.jsx · link.jsx
|
|
146
|
+
_helpers.js · index.js
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Exports
|
|
150
|
+
|
|
151
|
+
`XeplrTable`, `useTableController`, `useActionsController`, `TYPES`, `detectTypes`, `buildChangeSet`, `resolveCellStyle`, `resolveOperator`, `CHILD_DISPLAY`, individual filter components, action components, `renderers` registry + each renderer factory.
|
package/package.json
CHANGED
|
@@ -1,15 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xeplr/ui-table",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Controlled TanStack Table wrapper with auto-detected column types and smart filters",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
|
-
"
|
|
8
|
-
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "for f in test/*.test.js; do node \"$f\" || exit 1; done"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src/"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"table",
|
|
15
|
+
"tanstack",
|
|
16
|
+
"react",
|
|
17
|
+
"filters",
|
|
18
|
+
"datagrid"
|
|
19
|
+
],
|
|
9
20
|
"author": "xeplr",
|
|
10
21
|
"license": "MIT",
|
|
11
|
-
"repository": {
|
|
12
|
-
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/Xeplr/xeplr-ui-table"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
13
29
|
"peerDependencies": {
|
|
14
30
|
"react": "^18.0.0 || ^19.0.0",
|
|
15
31
|
"@tanstack/react-table": "^8.0.0"
|
package/src/CellZoom.jsx
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import React, { useRef, useEffect, useLayoutEffect } from 'react';
|
|
2
|
+
import { flexRender } from '@tanstack/react-table';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A cell, popped out and enlarged.
|
|
6
|
+
*
|
|
7
|
+
* The problem it solves is plain readability: report grids run small type so
|
|
8
|
+
* more fits on a page, and a long value in a narrow column wraps to three
|
|
9
|
+
* cramped lines. Double-clicking gives you that one value at a size you can
|
|
10
|
+
* actually read, without changing the table's font for everyone or making the
|
|
11
|
+
* column wider.
|
|
12
|
+
*
|
|
13
|
+
* It renders the column's OWN cell renderer, not the raw value — so a currency
|
|
14
|
+
* cell zooms as ₹36,260.80 and a badge zooms as a badge. Seeing something
|
|
15
|
+
* different up close from what was on the page would defeat the point.
|
|
16
|
+
*
|
|
17
|
+
* Deliberately not a modal: it doesn't trap focus or block the page, because
|
|
18
|
+
* it's a reading aid, not a task. Escape, a click anywhere else, or scrolling
|
|
19
|
+
* the table all dismiss it.
|
|
20
|
+
*/
|
|
21
|
+
export default function CellZoom({ cell, anchorRect, font, onClose }) {
|
|
22
|
+
var boxRef = useRef(null);
|
|
23
|
+
|
|
24
|
+
// Position after render, once the box's real size is known — a value's
|
|
25
|
+
// width isn't predictable before it's laid out at the larger size.
|
|
26
|
+
useLayoutEffect(function() {
|
|
27
|
+
var box = boxRef.current;
|
|
28
|
+
if (!box || !anchorRect) return;
|
|
29
|
+
var margin = 8;
|
|
30
|
+
var rect = box.getBoundingClientRect();
|
|
31
|
+
|
|
32
|
+
// Grows out of the cell it came from, so the eye doesn't lose its place.
|
|
33
|
+
var left = anchorRect.left;
|
|
34
|
+
var top = anchorRect.top;
|
|
35
|
+
if (left + rect.width > window.innerWidth - margin) left = window.innerWidth - rect.width - margin;
|
|
36
|
+
if (top + rect.height > window.innerHeight - margin) top = anchorRect.bottom - rect.height;
|
|
37
|
+
box.style.left = Math.max(margin, left) + 'px';
|
|
38
|
+
box.style.top = Math.max(margin, top) + 'px';
|
|
39
|
+
box.focus();
|
|
40
|
+
}, [anchorRect]);
|
|
41
|
+
|
|
42
|
+
useEffect(function() {
|
|
43
|
+
function onKey(e) { if (e.key === 'Escape') onClose(); }
|
|
44
|
+
function onPointer(e) { if (boxRef.current && !boxRef.current.contains(e.target)) onClose(); }
|
|
45
|
+
// Scroll closes rather than re-anchoring: a box chasing its cell across a
|
|
46
|
+
// scrolling table is harder to read than one that simply gets out of the way.
|
|
47
|
+
document.addEventListener('keydown', onKey);
|
|
48
|
+
document.addEventListener('mousedown', onPointer);
|
|
49
|
+
window.addEventListener('scroll', onClose, true);
|
|
50
|
+
return function() {
|
|
51
|
+
document.removeEventListener('keydown', onKey);
|
|
52
|
+
document.removeEventListener('mousedown', onPointer);
|
|
53
|
+
window.removeEventListener('scroll', onClose, true);
|
|
54
|
+
};
|
|
55
|
+
}, [onClose]);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div
|
|
59
|
+
ref={boxRef}
|
|
60
|
+
className="xeplr-table-cell-zoom"
|
|
61
|
+
role="dialog"
|
|
62
|
+
aria-label="Enlarged cell"
|
|
63
|
+
tabIndex={-1}
|
|
64
|
+
style={{
|
|
65
|
+
minWidth: anchorRect ? Math.min(anchorRect.width, 320) + 'px' : undefined,
|
|
66
|
+
// Everything inside is sized in em, so this one value sets the scale.
|
|
67
|
+
fontSize: font ? font.fontSize : undefined,
|
|
68
|
+
fontFamily: font ? font.fontFamily : undefined
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
<div className="xeplr-table-cell-zoom-label">{String(cell.column.columnDef.header || '')}</div>
|
|
72
|
+
<div className="xeplr-table-cell-zoom-value">
|
|
73
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|