Skip to content

Commit 8385091

Browse files
committed
release: @datasketch/monkeytab@0.6.0
1 parent fbdb61c commit 8385091

14 files changed

Lines changed: 451 additions & 25 deletions

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,7 @@ deno.lock
1212
.local/
1313
CLAUDE.md
1414
.claude/
15+
# Sync-owner-only config, regenerated on every sync. Public
16+
# contributors don't have .local/, so committing it would give
17+
# them broken references. See CLAUDE.md for context.
18+
deno.json

BROWSER.md

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,82 @@ interface MonkeyTableColumn {
174174
maxWidth?: number; // maximum width (default: 600)
175175
sortable?: boolean; // can this column be sorted (default: true)
176176
align?: 'left' | 'center' | 'right'; // cell text alignment
177+
color?: string | CellColorFn; // column color — static string or per-cell fn
177178
}
179+
180+
type CellColorFn = (
181+
row: Record<string, Value>,
182+
value: Value,
183+
fieldId: string,
184+
) => string | undefined;
178185
```
179186

187+
### Column coloring (`column.color`)
188+
189+
Three shapes, from simplest to most flexible:
190+
191+
- **Static** (`string`) — any CSS color. Every cell in the column plus the header gets a soft tint of that color. Good for marking a non-editable column, flagging a column that needs attention, or grouping related columns visually.
192+
- **Rule array** (`ColorRule[]`) — JSON-serializable conditional formatting. Each rule is `{ when: ColorCondition, color: string }`. Rules evaluate top-down; first match wins. Header stays default in this mode. Works in the JSON config form.
193+
- **Function** (`CellColorFn`) — `(row, value, fieldId) => string | undefined`. For anything the rule array can't express: palettes, numeric interpolation, cross-field math. Header stays default. Prop-API only (functions aren't JSON-serializable).
194+
195+
Tints always blend with `color-mix(… 30%, white)` so dark colors stay readable, and compose cleanly with the row-level `colorBy` tint and the in-search yellow highlight.
196+
197+
```tsx
198+
<MonkeyTable
199+
columns={[
200+
{ id: 'Name' },
201+
// Static: the whole "CreatedAt" column + header reads as a read-only band.
202+
{ id: 'CreatedAt', type: 'Date', editable: false, color: '#f1f5f9' },
203+
// Rule array: red < 50, green ≥ 90, else untouched. Also works in JSON config.
204+
{
205+
id: 'Score',
206+
type: 'Number',
207+
color: [
208+
{ when: { op: 'lt', value: 50 }, color: '#fee2e2' },
209+
{ when: { op: 'gte', value: 90 }, color: '#dcfce7' },
210+
],
211+
},
212+
// Function: same effect but with a mid-range yellow band, and arbitrary logic.
213+
{
214+
id: 'Priority',
215+
color: (_row, value) => {
216+
if (value === 'P0') return '#fecaca';
217+
if (value === 'P1') return '#fed7aa';
218+
return undefined;
219+
},
220+
},
221+
]}
222+
rows={rows}
223+
/>
224+
```
225+
226+
**Rule operators.** `ColorCondition` is a tagged union keyed by `op`:
227+
228+
| op | value(s) | Matches when |
229+
|---|---|---|
230+
| `equals` / `notEquals` | `value: unknown` | strict `===` / `!==` |
231+
| `lt` / `lte` / `gt` / `gte` | `value: number` | numeric comparison (non-numeric cells never match) |
232+
| `contains` / `notContains` | `value: string` | case-insensitive substring on string cells |
233+
| `empty` / `notEmpty` || `null`, `undefined`, `""`, or `[]` |
234+
| `in` / `notIn` | `values: unknown[]` | value is / isn't one of the list |
235+
236+
Every operator accepts an optional `field: string` to compare against a **different** column's value instead of the cell's own — useful for "tint the `Status` cell when `Owner` is empty":
237+
238+
```ts
239+
color: [
240+
{ when: { op: 'empty', field: 'Owner' }, color: '#fef3c7' },
241+
]
242+
```
243+
244+
For callers who want to reuse the same evaluator outside the grid (e.g., in a summary widget), `evaluateColorRules(rules, row, value)` is exported too:
245+
246+
```ts
247+
import { evaluateColorRules } from '@datasketch/monkeytab';
248+
const tint = evaluateColorRules(col.color as ColorRule[], row, row.Score);
249+
```
250+
251+
In the JSON config form (`<MonkeyTableFromConfig>`), `string` and `ColorRule[]` are both accepted. The function form stays prop-API-only.
252+
180253
---
181254

182255
## Field Types
@@ -243,6 +316,7 @@ const config: MonkeyTableConfig = {
243316
interface MonkeyTableConfig {
244317
schemaVersion?: number; // reserved for future migrators
245318
columns: MonkeyTableConfigColumn[]; // same as MonkeyTableColumn, minus render/icon
319+
// and with color narrowed to string
246320
rows: Array<Record<string, Value>>;
247321
settings?: MonkeyTableConfigSettings;
248322
}
@@ -252,7 +326,9 @@ interface MonkeyTableConfig {
252326
above — `editable`, `height`, `pageSize`, `locale`, `groupBy`, `sortBy`,
253327
`dateDisplayFormat`, etc. Anything not serializable (React handlers, custom
254328
renderers/editors, `render`/`icon` on a column, `functions`/`constraints`,
255-
`presence`) stays as ordinary component props on `<MonkeyTableFromConfig>`.
329+
`presence`, conditional-coloring functions) stays as ordinary component props
330+
on `<MonkeyTableFromConfig>`. Static `column.color` (CSS string) is allowed
331+
in the config; the function form is prop-API-only.
256332

257333
### resolveConfig
258334

CHANGELOG.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66

77
## [Unreleased]
88

9+
## [0.6.0] — 2026-04-19
10+
11+
### New
12+
- **Column coloring (`column.color`)** — three shapes, same prop. Pass a CSS color string to tint a whole column (every cell + the header) with a soft blend — good for flagging a non-editable column or grouping related columns visually. Pass a `ColorRule[]` (`{ when: { op, value/values, field? }, color }`) for JSON-serializable Google-Sheets-style conditional formatting; rules evaluate top-down and the first match wins. Operators include `equals`/`notEquals`, `lt`/`lte`/`gt`/`gte`, `contains`/`notContains`, `empty`/`notEmpty`, and `in`/`notIn`; each can optionally compare a different field. For anything the rule array can't express (palettes, numeric interpolation, cross-field math), pass a `(row, value, fieldId) => string | undefined` function instead — prop-API only. The evaluator is exported as `evaluateColorRules(rules, row, value)` so the same rules can be reused outside the grid. See [BROWSER.md → Column coloring](./BROWSER.md#column-coloring-columncolor).
13+
14+
### Fixed
15+
- **Header overflow fade now tints with the column** — when a column uses `color` to tint its header, the right-edge fade gradient blends into the tinted background instead of showing a strip of default gray. Symmetric with the row-background fade that already respected `colorBy`.
16+
917
## [0.5.0] — 2026-04-19
1018

1119
### New
@@ -118,7 +126,8 @@ First public release.
118126
- `onUpload` prop — bring your own file upload (S3, Cloudinary, etc.)
119127
- Drag-and-drop and paste support for Image cells
120128

121-
[Unreleased]: https://github.com/datasketch/monkeytab/compare/v0.5.0...HEAD
129+
[Unreleased]: https://github.com/datasketch/monkeytab/compare/v0.6.0...HEAD
130+
[0.6.0]: https://github.com/datasketch/monkeytab/compare/v0.5.0...v0.6.0
122131
[0.5.0]: https://github.com/datasketch/monkeytab/compare/v0.4.0...v0.5.0
123132
[0.4.0]: https://github.com/datasketch/monkeytab/compare/v0.3.0...v0.4.0
124133
[0.3.0]: https://github.com/datasketch/monkeytab/compare/v0.2.1...v0.3.0

examples/browser-standalone/main.tsx

Lines changed: 119 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,36 @@ const COLUMNS = [
4747

4848
const COLUMN_OPTIONS_COLUMNS = [
4949
{ id: 'Product', width: 200, minWidth: 150, maxWidth: 300 },
50-
{ id: 'SKU', width: 120, editable: false, sortable: false },
50+
// Static `color`: the whole SKU column (including its header) reads as a soft
51+
// read-only band. Good for flagging columns the user shouldn't touch.
52+
{ id: 'SKU', width: 120, editable: false, sortable: false, color: '#f1f5f9' },
5153
{ id: 'Category', type: 'SingleSelect' as const, width: 140, options: { options: [
5254
{ value: 'Electronics', label: 'Electronics', color: '#dbeafe' },
5355
{ value: 'Clothing', label: 'Clothing', color: '#fce7f3' },
5456
{ value: 'Food', label: 'Food', color: '#dcfce7' },
5557
{ value: 'Books', label: 'Books', color: '#fef3c7' },
5658
]}},
57-
{ id: 'Price', type: 'Number' as const, width: 110, align: 'right' as const, options: { precision: 2 } },
58-
{ id: 'Stock', type: 'Number' as const, width: 90, align: 'center' as const, editable: false },
59+
// Rule-array `color`: JSON-serializable conditional formatting. Rules evaluate
60+
// top-down — the first match wins. Red for high-ticket items, pale yellow for
61+
// cheap ones. Works in `<MonkeyTableFromConfig>` too (same JSON shape).
62+
{ id: 'Price', type: 'Number' as const, width: 110, align: 'right' as const, options: { precision: 2 },
63+
color: [
64+
{ when: { op: 'gte' as const, value: 100 }, color: '#fee2e2' },
65+
{ when: { op: 'lt' as const, value: 25 }, color: '#fef9c3' },
66+
],
67+
},
68+
// Function `color`: escape hatch for logic the rule array can't express —
69+
// here, a three-tier band built from the numeric value. Header stays default
70+
// (no row to evaluate against). Prop-API only (not JSON-serializable).
71+
{ id: 'Stock', type: 'Number' as const, width: 90, align: 'center' as const, editable: false,
72+
color: (_row: Record<string, unknown>, value: unknown) => {
73+
if (typeof value !== 'number') return undefined;
74+
if (value === 0) return '#fecaca'; // out of stock
75+
if (value < 100) return '#fed7aa'; // low
76+
if (value > 500) return '#bbf7d0'; // overstocked
77+
return undefined;
78+
},
79+
},
5980
{ id: 'Available', type: 'Boolean' as const, width: 100, align: 'center' as const },
6081
{ id: 'Notes', width: 250, minWidth: 150, maxWidth: 500 },
6182
];
@@ -980,9 +1001,49 @@ function App() {
9801001
const [ghostGrid, setGhostGrid] = useState(true);
9811002
const [groupBy, setGroupBy] = useState<string | null>(null);
9821003
const [colorBy, setColorBy] = useState<string | null>(null);
1004+
// Runtime column-coloring: pick a column id + a CSS color and it's applied
1005+
// as a static `column.color` string on the active tab's table. Demonstrates
1006+
// that the prop works live; same shape you'd put in a JSON config.
1007+
const [userColumnId, setUserColumnId] = useState<string | null>(null);
1008+
const [userColumnColor, setUserColumnColor] = useState<string>('#fde68a');
9831009

9841010
const locale = language === 'es' ? 'es-CO' : 'en-US';
9851011

1012+
// Which column set the toolbar controls (Row color + Column color) should
1013+
// address for the currently-active tab. Tabs not listed here are wired
1014+
// inside sub-components — the toolbar dropdowns will show "None" only and
1015+
// clear when the user switches to them.
1016+
const TAB_COLUMNS: Record<string, ReadonlyArray<{ id: string; type?: string }>> = useMemo(() => ({
1017+
editable: COLUMNS,
1018+
columns: COLUMN_OPTIONS_COLUMNS,
1019+
readonly: METRICS_COLUMNS,
1020+
}), []);
1021+
const activeColumnSet = TAB_COLUMNS[tab] ?? [];
1022+
1023+
// If the selected column / colorBy field doesn't exist on the new tab,
1024+
// silently clear — the control shouldn't dangle a stale selection.
1025+
useEffect(() => {
1026+
if (userColumnId && !activeColumnSet.some((c) => c.id === userColumnId)) {
1027+
setUserColumnId(null);
1028+
}
1029+
if (colorBy && !activeColumnSet.some((c) => c.id === colorBy)) {
1030+
setColorBy(null);
1031+
}
1032+
}, [tab, activeColumnSet, userColumnId, colorBy]);
1033+
1034+
// Overlay the user's color onto the active tab's columns. Leaves everything
1035+
// else alone (including per-column `color` that was already set in source —
1036+
// the user's pick only wins for the column they picked).
1037+
const overlayUserColor = <T extends { id: string }>(cols: ReadonlyArray<T>): T[] => {
1038+
if (!userColumnId) return cols as T[];
1039+
return cols.map((c) =>
1040+
c.id === userColumnId ? ({ ...c, color: userColumnColor } as T) : c,
1041+
);
1042+
};
1043+
const editableColumns = useMemo(() => overlayUserColor(COLUMNS), [userColumnId, userColumnColor]);
1044+
const columnsTabColumns = useMemo(() => overlayUserColor(COLUMN_OPTIONS_COLUMNS), [userColumnId, userColumnColor]);
1045+
const readonlyTabColumns = useMemo(() => overlayUserColor(METRICS_COLUMNS), [userColumnId, userColumnColor]);
1046+
9861047
const handleSortChange = (fieldId: string | null, direction: 'asc' | 'desc' | null) => {
9871048
setSortBy(fieldId);
9881049
setSortDirection(direction);
@@ -1042,20 +1103,58 @@ function App() {
10421103
</select>
10431104
</label>
10441105
<label style={{ fontSize: '14px', display: 'inline-flex', alignItems: 'center', gap: '6px' }}>
1045-
Color:
1106+
Row color:
10461107
<select
10471108
value={colorBy ?? ''}
10481109
onChange={(e) => setColorBy(e.target.value || null)}
10491110
style={{ padding: '2px 6px', fontSize: '13px', borderRadius: '4px', border: '1px solid #d1d5db' }}
1111+
disabled={activeColumnSet.length === 0}
1112+
title={activeColumnSet.length === 0
1113+
? 'This tab runs inside a sub-component — row coloring is wired there, not at the toolbar'
1114+
: 'Row background tint from a SingleSelect / MultiSelect / Boolean field'}
10501115
>
10511116
<option value="">None</option>
1052-
{COLUMNS
1117+
{activeColumnSet
10531118
.filter((c) => (['SingleSelect', 'MultiSelect', 'Boolean'] as string[]).includes(c.type as string))
10541119
.map((c) => (
10551120
<option key={c.id} value={c.id}>{c.id}</option>
10561121
))}
10571122
</select>
10581123
</label>
1124+
<label style={{ fontSize: '14px', display: 'inline-flex', alignItems: 'center', gap: '6px' }}>
1125+
Column color:
1126+
<select
1127+
value={userColumnId ?? ''}
1128+
onChange={(e) => setUserColumnId(e.target.value || null)}
1129+
style={{ padding: '2px 6px', fontSize: '13px', borderRadius: '4px', border: '1px solid #d1d5db' }}
1130+
disabled={activeColumnSet.length === 0}
1131+
title={activeColumnSet.length === 0
1132+
? 'This tab runs inside a sub-component — column coloring is wired there, not at the toolbar'
1133+
: 'Pick a column to tint (header + all cells) with the color next to this dropdown'}
1134+
>
1135+
<option value="">None</option>
1136+
{activeColumnSet.map((c) => (
1137+
<option key={c.id} value={c.id}>{c.id}</option>
1138+
))}
1139+
</select>
1140+
<input
1141+
type="color"
1142+
value={userColumnColor}
1143+
onChange={(e) => setUserColumnColor(e.target.value)}
1144+
disabled={!userColumnId}
1145+
style={{
1146+
width: '28px',
1147+
height: '24px',
1148+
padding: 0,
1149+
border: '1px solid #d1d5db',
1150+
borderRadius: '4px',
1151+
cursor: userColumnId ? 'pointer' : 'not-allowed',
1152+
opacity: userColumnId ? 1 : 0.4,
1153+
background: 'none',
1154+
}}
1155+
title={userColumnId ? `Tint the "${userColumnId}" column with this color` : 'Pick a column first'}
1156+
/>
1157+
</label>
10591158
{tab === 'editable' && (
10601159
<>
10611160
<span style={{ fontSize: '14px', color: '#6b7280' }}>
@@ -1082,7 +1181,7 @@ function App() {
10821181
<div className="desc">Click cells to edit. Add/delete rows. Drag columns to reorder. Try the filter and search.</div>
10831182
<div className="table-container">
10841183
<MonkeyTable
1085-
columns={COLUMNS}
1184+
columns={editableColumns}
10861185
rows={INITIAL_ROWS}
10871186
onChange={handleChange}
10881187
sortBy={sortBy}
@@ -1146,12 +1245,22 @@ function App() {
11461245
<div className="example" style={{ display: 'flex', flexDirection: 'column', minHeight: 'calc(100vh - 200px)' }}>
11471246
<h2>Column Options</h2>
11481247
<div className="desc">
1149-
Per-column <code>width</code>, <code>minWidth</code>, <code>maxWidth</code>, <code>align</code>, <code>editable</code>, and <code>sortable</code>.
1150-
SKU and Stock are read-only. SKU is not sortable. Price and Stock are right/center-aligned.
1248+
Per-column <code>width</code>, <code>minWidth</code>, <code>maxWidth</code>, <code>align</code>, <code>editable</code>, <code>sortable</code>, and
1249+
{' '}<strong><code>color</code></strong>.
1250+
SKU and Stock are read-only; SKU is not sortable; Price and Stock are right/center-aligned.
1251+
<br />
1252+
<br />
1253+
<strong>Column coloring demo</strong> — three shapes, one prop:
1254+
<ul style={{ margin: '6px 0 0 1.2em', padding: 0 }}>
1255+
<li><strong>Static</strong>: <code>SKU</code> uses <code>color: '#f1f5f9'</code> — whole column + header get a soft read-only band.</li>
1256+
<li><strong>Rule array</strong>: <code>Price</code> uses <code>color: [&#123; when: &#123; op: 'gte', value: 100 &#125;, color: '#fee2e2' &#125;, &#123; when: &#123; op: 'lt', value: 25 &#125;, color: '#fef9c3' &#125;]</code>. Top-down, first match wins. JSON-serializable, so this works verbatim in <code>&lt;MonkeyTableFromConfig&gt;</code>.</li>
1257+
<li><strong>Function</strong>: <code>Stock</code> uses <code>color: (row, value) =&gt; …</code> with a three-tier band (red at 0, orange &lt; 100, green &gt; 500). Escape hatch for anything rule data can't express.</li>
1258+
</ul>
1259+
Tints compose with row-level coloring (the <em>Row color</em> dropdown on the Editable Table tab) and the search yellow highlight. Higher-priority states — selection, range, drag, Computed fields — still override.
11511260
</div>
11521261
<div className="table-container" style={{ flex: 1, height: 'auto' }}>
11531262
<MonkeyTable
1154-
columns={COLUMN_OPTIONS_COLUMNS}
1263+
columns={columnsTabColumns}
11551264
rows={COLUMN_OPTIONS_ROWS}
11561265
ghostGrid={ghostGrid}
11571266
height="100%"
@@ -1175,7 +1284,7 @@ function App() {
11751284
<div className="desc">Set <code>editable=false</code> to display data without mutation controls.</div>
11761285
<div className="table-container">
11771286
<MonkeyTable
1178-
columns={METRICS_COLUMNS}
1287+
columns={readonlyTabColumns}
11791288
rows={METRICS_ROWS}
11801289
editable={false}
11811290
height="100%"

examples/browser-standalone/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "browser-standalone",
3-
"version": "0.5.0",
3+
"version": "0.6.0",
44
"private": true,
55
"type": "module",
66
"scripts": {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@datasketch/monkeytab",
3-
"version": "0.5.0",
3+
"version": "0.6.0",
44
"description": "Embeddable, editable React table component",
55
"keywords": [
66
"react",

0 commit comments

Comments
 (0)