@xeplr/ui-table 1.0.0 → 1.0.1

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,28 @@
1
1
  {
2
2
  "name": "@xeplr/ui-table",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
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
+ "files": [
8
+ "src/"
9
+ ],
10
+ "keywords": [
11
+ "table",
12
+ "tanstack",
13
+ "react",
14
+ "filters",
15
+ "datagrid"
16
+ ],
9
17
  "author": "xeplr",
10
18
  "license": "MIT",
11
- "repository": { "type": "git", "url": "https://github.com/Xeplr/xeplr-ui-table" },
12
- "publishConfig": { "access": "public" },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/Xeplr/xeplr-ui-table"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
13
26
  "peerDependencies": {
14
27
  "react": "^18.0.0 || ^19.0.0",
15
28
  "@tanstack/react-table": "^8.0.0"
@@ -41,6 +41,11 @@ var CHILD_DISPLAY = { POPUP: 'popup', INNER: 'inner' };
41
41
  * @param {boolean} [props.enablePagination] - Default: true
42
42
  * @param {string} [props.className] - Additional CSS class
43
43
  * @param {Function} [props.onCommit] - async (changeSet[]) => void
44
+ * @param {Array<{key: string, label: string, icon?: any, onClick: (row) => void,
45
+ * visible?: (row) => boolean, disabled?: (row) => boolean, variant?: string}>} [props.rowActions]
46
+ * - Custom per-row action buttons (e.g. Rollback, Delete), replacing the built-in
47
+ * view/copy/edit/delete set. Fires immediately via each action's onClick — works
48
+ * standalone, without onCommit.
44
49
  */
45
50
  export default function XeplrTable(props) {
46
51
  var schema = props.schema || {};
@@ -85,10 +90,12 @@ export default function XeplrTable(props) {
85
90
  // Show expand column only in inner mode
86
91
  var showExpandCol = hasChildren && useInner;
87
92
 
93
+ var hasRowActions = actions.hasActions || !!(props.rowActions && props.rowActions.length > 0);
94
+
88
95
  var totalColumns = headerGroups[0]?.headers.length || 1;
89
96
  if (showExpandCol) totalColumns++;
90
97
  if (actions.hasDelete) totalColumns++;
91
- if (actions.hasActions) totalColumns++;
98
+ if (hasRowActions) totalColumns++;
92
99
 
93
100
  // Resolve modal schema columns for form hints
94
101
  var modalSchemaColumns = columns;
@@ -187,7 +194,7 @@ export default function XeplrTable(props) {
187
194
  </th>
188
195
  );
189
196
  })}
190
- {actions.hasActions && (
197
+ {hasRowActions && (
191
198
  <th className="xeplr-table-th xeplr-table-th-actions">
192
199
  <div className="xeplr-table-header-cell">
193
200
  <span className="xeplr-table-header-label">Actions</span>
@@ -247,9 +254,10 @@ export default function XeplrTable(props) {
247
254
  </td>
248
255
  );
249
256
  })}
250
- {actions.hasActions && (
257
+ {hasRowActions && (
251
258
  <td className="xeplr-table-td xeplr-table-td-actions">
252
259
  <ActionsCell row={original} hasSave={actions.hasSave} hasDelete={actions.hasDelete}
260
+ rowActions={props.rowActions}
253
261
  onView={function() { usePopup ? actions.openDetail(original) : actions.openView(original); }}
254
262
  onCopy={actions.openCopy} onEdit={actions.openEdit}
255
263
  onDelete={function() { actions.handleDeleteRow(rowId); }} />
@@ -2,7 +2,15 @@ import React from 'react';
2
2
 
3
3
  /**
4
4
  * Actions cell rendered in the last column of each row.
5
- * Shows view (always), copy, edit (if onSave), delete (if onDelete).
5
+ *
6
+ * Two modes:
7
+ * - props.rowActions given: renders exactly those custom actions, firing
8
+ * immediately via each action's own onClick (no staged CRUD queue).
9
+ * - otherwise: the built-in set — view (always), copy, edit (if onSave),
10
+ * delete (if onDelete) — driven by the table's onCommit staged queue.
11
+ *
12
+ * @param {Array<{key: string, label: string, icon?: any, onClick: (row) => void,
13
+ * visible?: (row) => boolean, disabled?: (row) => boolean, variant?: string}>} [props.rowActions]
6
14
  */
7
15
  export default function ActionsCell(props) {
8
16
  var row = props.row;
@@ -12,6 +20,33 @@ export default function ActionsCell(props) {
12
20
  var onCopy = props.onCopy;
13
21
  var onEdit = props.onEdit;
14
22
  var onDelete = props.onDelete;
23
+ var rowActions = props.rowActions;
24
+
25
+ if (rowActions && rowActions.length > 0) {
26
+ return (
27
+ <div className="xeplr-table-actions-cell">
28
+ {rowActions.map(function(action) {
29
+ if (action.visible && !action.visible(row)) return null;
30
+ var disabled = action.disabled ? action.disabled(row) : false;
31
+ var className = 'xeplr-table-action-btn xeplr-table-action-' + action.key
32
+ + (action.icon ? '' : ' xeplr-table-action-label')
33
+ + (action.variant ? ' xeplr-table-action-' + action.variant : '');
34
+ return (
35
+ <button
36
+ key={action.key}
37
+ type="button"
38
+ className={className}
39
+ title={action.label}
40
+ disabled={disabled}
41
+ onClick={function() { action.onClick(row); }}
42
+ >
43
+ {action.icon || action.label}
44
+ </button>
45
+ );
46
+ })}
47
+ </div>
48
+ );
49
+ }
15
50
 
16
51
  return (
17
52
  <div className="xeplr-table-actions-cell">
package/src/index.js CHANGED
@@ -30,3 +30,7 @@ export { resolve as resolveOperator, resolveString, resolveNumber, resolveDate,
30
30
 
31
31
  // Conditional formatting
32
32
  export { resolveCellStyle } from './resolveCellStyle.js';
33
+
34
+ // Cell renderers (config-driven)
35
+ export { default as renderers } from './renderers/index.js';
36
+ export { avatarName, tags, currency, memberChips, twoLine, statusBadge, dateDisplay, link } from './renderers/index.js';
@@ -0,0 +1,43 @@
1
+ export function initials(name) {
2
+ if (!name) return '·';
3
+ var parts = String(name).trim().split(/\s+/).slice(0, 2);
4
+ return parts.map(function(p) { return p.charAt(0).toUpperCase(); }).join('');
5
+ }
6
+
7
+ // Stable color per name — used by avatar tints.
8
+ var PALETTE = [
9
+ ['#d4af37', '#8a6f1f'],
10
+ ['#5b8def', '#2d4f99'],
11
+ ['#4ade80', '#1d7a3f'],
12
+ ['#ef5b5b', '#a02020'],
13
+ ['#a78bfa', '#5e3fa3'],
14
+ ['#f97316', '#a23f0a'],
15
+ ['#22d3ee', '#0d6e80'],
16
+ ['#fb7185', '#9b1c33']
17
+ ];
18
+
19
+ export function avatarTint(seed) {
20
+ var s = String(seed || '');
21
+ var hash = 0;
22
+ for (var i = 0; i < s.length; i++) hash = (hash * 31 + s.charCodeAt(i)) | 0;
23
+ var pair = PALETTE[Math.abs(hash) % PALETTE.length];
24
+ return 'linear-gradient(135deg, ' + pair[0] + ' 0%, ' + pair[1] + ' 100%)';
25
+ }
26
+
27
+ var CURRENCY_SYMBOLS = {
28
+ INR: '₹', USD: '$', EUR: '€', GBP: '£', JPY: '¥', AUD: 'A$', CAD: 'C$', SGD: 'S$', AED: 'د.إ'
29
+ };
30
+
31
+ export function currencySymbol(code) {
32
+ return CURRENCY_SYMBOLS[code] || code || '';
33
+ }
34
+
35
+ export function formatNumber(value, decimals) {
36
+ if (value === null || value === undefined || value === '') return '';
37
+ var n = Number(value);
38
+ if (!isFinite(n)) return '';
39
+ if (typeof decimals === 'number') {
40
+ return n.toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
41
+ }
42
+ return n.toLocaleString();
43
+ }
@@ -0,0 +1,31 @@
1
+ import { initials, avatarTint } from './_helpers.js';
2
+
3
+ /**
4
+ * config: { subKey?: string, size?: 'sm'|'md', imgKey?: string, imgPrefix?: string }
5
+ *
6
+ * If `imgKey` is set and the row has a value for that key, render an <img>;
7
+ * otherwise fall back to colored initials. `imgPrefix` is prepended to the
8
+ * value so callers can pass relative filenames and a base URL once.
9
+ */
10
+ export default function avatarName(config) {
11
+ var subKey = config && config.subKey;
12
+ var size = (config && config.size) || 'md';
13
+ var imgKey = config && config.imgKey;
14
+ var imgPrefix = (config && config.imgPrefix) || '';
15
+ return function cell(ctx) {
16
+ var name = ctx.getValue();
17
+ var sub = subKey ? ctx.row.original[subKey] : null;
18
+ var img = imgKey ? ctx.row.original[imgKey] : null;
19
+ return (
20
+ <div className={'xeplr-r-avatarname xeplr-r-avatarname-' + size}>
21
+ {img
22
+ ? <img className="xeplr-r-avatar xeplr-r-avatar-img" src={imgPrefix + img} alt="" />
23
+ : <div className="xeplr-r-avatar" style={{ background: avatarTint(name) }}>{initials(name)}</div>}
24
+ <div className="xeplr-r-avatarname-text">
25
+ <div className="xeplr-r-avatarname-name">{name}</div>
26
+ {sub && <div className="xeplr-r-avatarname-sub">{sub}</div>}
27
+ </div>
28
+ </div>
29
+ );
30
+ };
31
+ }
@@ -0,0 +1,25 @@
1
+ import { currencySymbol, formatNumber } from './_helpers.js';
2
+
3
+ /**
4
+ * config: { currency?: 'INR'|'USD'|..., position?: 'before'|'after', decimals?: number, bold?: boolean }
5
+ */
6
+ export default function currency(config) {
7
+ var code = (config && config.currency) || 'INR';
8
+ var position = (config && config.position) || 'before';
9
+ var decimals = config && typeof config.decimals === 'number' ? config.decimals : 0;
10
+ var bold = config && config.bold !== false;
11
+ var symbol = currencySymbol(code);
12
+ return function cell(ctx) {
13
+ var v = ctx.getValue();
14
+ if (v === null || v === undefined || v === '') return null;
15
+ var formatted = formatNumber(v, decimals);
16
+ var className = 'xeplr-r-currency' + (bold ? ' xeplr-r-currency-bold' : '');
17
+ return (
18
+ <span className={className}>
19
+ {position === 'before' && <span className="xeplr-r-currency-sym">{symbol}</span>}
20
+ <span className="xeplr-r-currency-value">{formatted}</span>
21
+ {position === 'after' && <span className="xeplr-r-currency-sym xeplr-r-currency-sym-after">{symbol}</span>}
22
+ </span>
23
+ );
24
+ };
25
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * config: { format?: 'date'|'datetime'|'relative' }
3
+ */
4
+ function pad(n) { return n < 10 ? '0' + n : '' + n; }
5
+ var MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
6
+
7
+ function fmtDate(d) {
8
+ return pad(d.getDate()) + ' ' + MONTHS[d.getMonth()] + ' ' + d.getFullYear();
9
+ }
10
+
11
+ function fmtDateTime(d) {
12
+ return fmtDate(d) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes());
13
+ }
14
+
15
+ function fmtRelative(d, now) {
16
+ var diff = (now - d) / 1000;
17
+ var abs = Math.abs(diff);
18
+ if (abs < 60) return 'just now';
19
+ if (abs < 3600) return Math.round(abs / 60) + 'm ' + (diff > 0 ? 'ago' : 'from now');
20
+ if (abs < 86400) return Math.round(abs / 3600) + 'h ' + (diff > 0 ? 'ago' : 'from now');
21
+ if (abs < 86400 * 30) return Math.round(abs / 86400) + 'd ' + (diff > 0 ? 'ago' : 'from now');
22
+ return fmtDate(d);
23
+ }
24
+
25
+ export default function dateDisplay(config) {
26
+ var format = (config && config.format) || 'date';
27
+ return function cell(ctx) {
28
+ var v = ctx.getValue();
29
+ if (!v) return null;
30
+ var d = v instanceof Date ? v : new Date(v);
31
+ if (isNaN(d.getTime())) return null;
32
+ var out;
33
+ if (format === 'datetime') out = fmtDateTime(d);
34
+ else if (format === 'relative') out = fmtRelative(d, new Date());
35
+ else out = fmtDate(d);
36
+ return <span className="xeplr-r-date">{out}</span>;
37
+ };
38
+ }
@@ -0,0 +1,22 @@
1
+ import avatarName from './avatarName.jsx';
2
+ import tags from './tags.jsx';
3
+ import currency from './currency.jsx';
4
+ import memberChips from './memberChips.jsx';
5
+ import twoLine from './twoLine.jsx';
6
+ import statusBadge from './statusBadge.jsx';
7
+ import dateDisplay from './dateDisplay.jsx';
8
+ import link from './link.jsx';
9
+
10
+ var renderers = {
11
+ avatarName: avatarName,
12
+ tags: tags,
13
+ currency: currency,
14
+ memberChips: memberChips,
15
+ twoLine: twoLine,
16
+ statusBadge: statusBadge,
17
+ dateDisplay: dateDisplay,
18
+ link: link
19
+ };
20
+
21
+ export default renderers;
22
+ export { avatarName, tags, currency, memberChips, twoLine, statusBadge, dateDisplay, link };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * config: { onClick?: (row) => void, hrefKey?: string, target?: string }
3
+ *
4
+ * If `hrefKey` is set, renders an <a>. Otherwise a clickable span that calls onClick(row).
5
+ */
6
+ export default function link(config) {
7
+ var onClick = config && config.onClick;
8
+ var hrefKey = config && config.hrefKey;
9
+ var target = config && config.target;
10
+ return function cell(ctx) {
11
+ var label = ctx.getValue();
12
+ if (label === null || label === undefined || label === '') return null;
13
+ if (hrefKey) {
14
+ var href = ctx.row.original[hrefKey];
15
+ return <a className="xeplr-r-link" href={href} target={target}>{label}</a>;
16
+ }
17
+ return (
18
+ <span
19
+ className="xeplr-r-link"
20
+ onClick={function() { if (onClick) onClick(ctx.row.original); }}
21
+ >{label}</span>
22
+ );
23
+ };
24
+ }
@@ -0,0 +1,35 @@
1
+ import { initials, avatarTint } from './_helpers.js';
2
+
3
+ /**
4
+ * config: { max?: number, nameKey?: string, imgKey?: string, imgPrefix?: string }
5
+ * Value: Array<{ [nameKey]: string, [imgKey]?: string, ... }>
6
+ */
7
+ export default function memberChips(config) {
8
+ var max = (config && config.max) || 3;
9
+ var nameKey = (config && config.nameKey) || 'name';
10
+ var imgKey = config && config.imgKey;
11
+ var imgPrefix = (config && config.imgPrefix) || '';
12
+ return function cell(ctx) {
13
+ var value = ctx.getValue();
14
+ if (!Array.isArray(value) || value.length === 0) return null;
15
+ var visible = value.slice(0, max);
16
+ var overflow = Math.max(value.length - max, 0);
17
+ return (
18
+ <div className="xeplr-r-chips">
19
+ {visible.map(function(item, i) {
20
+ var name = item && item[nameKey] ? item[nameKey] : '';
21
+ var img = imgKey && item ? item[imgKey] : null;
22
+ return (
23
+ <span key={i} className="xeplr-r-chip">
24
+ {img
25
+ ? <img className="xeplr-r-chip-avatar xeplr-r-chip-avatar-img" src={imgPrefix + img} alt="" />
26
+ : <span className="xeplr-r-chip-avatar" style={{ background: avatarTint(name) }}>{initials(name)}</span>}
27
+ <span className="xeplr-r-chip-name">{name}</span>
28
+ </span>
29
+ );
30
+ })}
31
+ {overflow > 0 && <span className="xeplr-r-chip-more">+{overflow}</span>}
32
+ </div>
33
+ );
34
+ };
35
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * config: { map: { [value]: { bg?, color?, label? } }, default?: { bg?, color? } }
3
+ */
4
+ export default function statusBadge(config) {
5
+ var map = (config && config.map) || {};
6
+ var fallback = (config && config.default) || { bg: '#232540', color: '#b8bbe0' };
7
+ return function cell(ctx) {
8
+ var v = ctx.getValue();
9
+ if (v === null || v === undefined || v === '') return null;
10
+ var entry = map[v] || fallback;
11
+ var label = entry.label || String(v);
12
+ var style = { background: entry.bg, color: entry.color, borderColor: entry.border || entry.bg };
13
+ return <span className="xeplr-r-badge" style={style}>{label}</span>;
14
+ };
15
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * config: { variant?: 'gold'|'muted'|'blue', max?: number }
3
+ * Value: string[] (or string — split by comma)
4
+ */
5
+ export default function tags(config) {
6
+ var variant = (config && config.variant) || 'gold';
7
+ var max = config && config.max;
8
+ return function cell(ctx) {
9
+ var value = ctx.getValue();
10
+ if (!value) return null;
11
+ var list = Array.isArray(value) ? value : String(value).split(',').map(function(s) { return s.trim(); }).filter(Boolean);
12
+ var visible = max ? list.slice(0, max) : list;
13
+ var overflow = max ? Math.max(list.length - max, 0) : 0;
14
+ return (
15
+ <div className="xeplr-r-tags">
16
+ {visible.map(function(t, i) {
17
+ return <span key={i} className={'xeplr-r-tag xeplr-r-tag-' + variant}>{t}</span>;
18
+ })}
19
+ {overflow > 0 && <span className="xeplr-r-tag-more">+{overflow}</span>}
20
+ </div>
21
+ );
22
+ };
23
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * config: { subKey?: string, primaryClass?: string }
3
+ */
4
+ export default function twoLine(config) {
5
+ var subKey = config && config.subKey;
6
+ return function cell(ctx) {
7
+ var primary = ctx.getValue();
8
+ var sub = subKey ? ctx.row.original[subKey] : null;
9
+ return (
10
+ <div className="xeplr-r-twoline">
11
+ <div className="xeplr-r-twoline-primary">{primary}</div>
12
+ {sub && <div className="xeplr-r-twoline-sub">{sub}</div>}
13
+ </div>
14
+ );
15
+ };
16
+ }
@@ -14,6 +14,7 @@ import { stringFilterFn } from './filters/StringFilter.jsx';
14
14
  import { numberFilterFn } from './filters/NumberFilter.jsx';
15
15
  import { dateFilterFn } from './filters/DateFilter.jsx';
16
16
  import { booleanFilterFn } from './filters/BooleanFilter.jsx';
17
+ import renderers from './renderers/index.js';
17
18
 
18
19
  var filterFnMap = {
19
20
  [TYPES.STRING]: stringFilterFn,
@@ -82,7 +83,17 @@ export default function useTableController(options) {
82
83
  enableSorting: col.enableSorting !== false,
83
84
  enableColumnFilter: col.enableColumnFilter !== false
84
85
  };
85
- if (col.cell) colDef.cell = col.cell;
86
+ if (col.cell) {
87
+ colDef.cell = col.cell;
88
+ } else if (col.render) {
89
+ var rendererName = typeof col.render === 'string' ? col.render : col.render.type;
90
+ var rendererFn = renderers[rendererName];
91
+ if (!rendererFn) {
92
+ throw new Error('[xeplr-ui-table] Unknown renderer "' + rendererName + '". Available: ' + Object.keys(renderers).join(', '));
93
+ }
94
+ var rendererConfig = typeof col.render === 'string' ? {} : col.render;
95
+ colDef.cell = rendererFn(rendererConfig);
96
+ }
86
97
  return columnHelper.accessor(col.accessor, colDef);
87
98
  });
88
99
  }, [columns, detectedTypes]);