@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 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.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
- "files": ["src/"],
8
- "keywords": ["table", "tanstack", "react", "filters", "datagrid"],
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": { "type": "git", "url": "https://github.com/Xeplr/xeplr-ui-table" },
12
- "publishConfig": { "access": "public" },
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"
@@ -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
+ }