Skip to content

Commit fbdb61c

Browse files
committed
release v0.5.0
1 parent c8f1c61 commit fbdb61c

9 files changed

Lines changed: 527 additions & 4 deletions

File tree

BROWSER.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,77 @@ interface MonkeyTableColumn {
200200

201201
---
202202

203+
## Config-driven API
204+
205+
If you'd rather describe a whole table as one JSON blob — for example to store
206+
a user's table layout, serve a table from a REST endpoint, or have an LLM emit
207+
it from a sample of rows — use `<MonkeyTableFromConfig>`.
208+
209+
```tsx
210+
import { MonkeyTableFromConfig, type MonkeyTableConfig } from '@datasketch/monkeytab';
211+
212+
const config: MonkeyTableConfig = {
213+
columns: [
214+
{ id: 'Name' },
215+
{ id: 'Age', type: 'Number', options: { format: 'decimal' } },
216+
{ id: 'Signup', type: 'Date', options: { dateFormat: 'iso' } },
217+
{ id: 'Status', type: 'SingleSelect', options: {
218+
options: [
219+
{ value: 'active', label: 'Active', color: '#22c55e' },
220+
{ value: 'paused', label: 'Paused', color: '#f59e0b' },
221+
],
222+
} },
223+
{ id: 'Website', type: 'URL' },
224+
],
225+
rows: [
226+
{ Name: 'Alice', Age: 30, Signup: '2024-01-05', Status: 'active', Website: 'https://example.com' },
227+
{ Name: 'Bob', Age: 25, Signup: '2024-02-10', Status: 'paused', Website: 'https://example.org' },
228+
],
229+
settings: {
230+
editable: true,
231+
height: 'auto',
232+
pageSize: 25,
233+
locale: 'en-US',
234+
},
235+
};
236+
237+
<MonkeyTableFromConfig config={config} onChange={setRows} />
238+
```
239+
240+
### Shape
241+
242+
```ts
243+
interface MonkeyTableConfig {
244+
schemaVersion?: number; // reserved for future migrators
245+
columns: MonkeyTableConfigColumn[]; // same as MonkeyTableColumn, minus render/icon
246+
rows: Array<Record<string, Value>>;
247+
settings?: MonkeyTableConfigSettings;
248+
}
249+
```
250+
251+
`settings` is a flat bucket that mirrors the scalar/enum props documented
252+
above — `editable`, `height`, `pageSize`, `locale`, `groupBy`, `sortBy`,
253+
`dateDisplayFormat`, etc. Anything not serializable (React handlers, custom
254+
renderers/editors, `render`/`icon` on a column, `functions`/`constraints`,
255+
`presence`) stays as ordinary component props on `<MonkeyTableFromConfig>`.
256+
257+
### resolveConfig
258+
259+
If you'd rather keep using `<MonkeyTable>` directly, import `resolveConfig`
260+
to turn a config into the flat prop object it consumes:
261+
262+
```tsx
263+
import { MonkeyTable, resolveConfig } from '@datasketch/monkeytab';
264+
265+
const props = resolveConfig(config);
266+
<MonkeyTable {...props} onChange={setRows} />
267+
```
268+
269+
The two forms are interchangeable — `<MonkeyTableFromConfig>` is a thin
270+
forwardRef wrapper that does exactly this internally.
271+
272+
---
273+
203274
## Keyboard Shortcuts
204275

205276
| Key | Action |

CHANGELOG.md

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

77
## [Unreleased]
88

9+
## [0.5.0] — 2026-04-19
10+
11+
### New
12+
- **`<MonkeyTableFromConfig>` + `resolveConfig()`** — describe a whole table as one JSON blob (`{ columns, rows, settings }`) and render it with a single component. The settings bucket mirrors the flat `<MonkeyTable>` props exactly; anything not serializable (handlers, custom renderers/editors, per-column `render`/`icon`) stays as ordinary React props on the wrapper. Use `resolveConfig(config)` to get the same props object if you'd rather keep using `<MonkeyTable>` directly. Useful for persisting user layouts, embedding tables in REST payloads, or having an LLM emit a table from a row sample. See [BROWSER.md → Config-driven API](./BROWSER.md#config-driven-api).
13+
914
## [0.4.0] — 2026-04-18
1015

1116
### New
@@ -113,7 +118,8 @@ First public release.
113118
- `onUpload` prop — bring your own file upload (S3, Cloudinary, etc.)
114119
- Drag-and-drop and paste support for Image cells
115120

116-
[Unreleased]: https://github.com/datasketch/monkeytab/compare/v0.4.0...HEAD
121+
[Unreleased]: https://github.com/datasketch/monkeytab/compare/v0.5.0...HEAD
122+
[0.5.0]: https://github.com/datasketch/monkeytab/compare/v0.4.0...v0.5.0
117123
[0.4.0]: https://github.com/datasketch/monkeytab/compare/v0.3.0...v0.4.0
118124
[0.3.0]: https://github.com/datasketch/monkeytab/compare/v0.2.1...v0.3.0
119125
[0.2.1]: https://github.com/datasketch/monkeytab/compare/v0.2.0...v0.2.1

examples/browser-standalone/main.tsx

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import React, { useState, useMemo, useRef, useEffect } from 'react';
88
import { createRoot } from 'react-dom/client';
9-
import { MonkeyTable, type MonkeyTableHandle } from '@monkeytab/browser';
9+
import { MonkeyTable, MonkeyTableFromConfig, type MonkeyTableHandle, type MonkeyTableConfig } from '@monkeytab/browser';
1010
import type { Value, PresenceUser, RemoteChangeEvent } from '@monkeytab/browser';
1111

1212
// ---------------------------------------------------------------------------
@@ -833,8 +833,131 @@ function applyToLocalRows(
833833
}
834834
}
835835

836+
// ---------------------------------------------------------------------------
837+
// Example: Config-driven table — one JSON blob renders the whole thing
838+
//
839+
// Shows <MonkeyTableFromConfig> in action. Everything you'd normally spread
840+
// across columns + props + options lives in one serializable object that
841+
// round-trips through JSON.stringify. Non-serializable pieces (here: onChange)
842+
// stay as ordinary React props on the wrapper.
843+
// ---------------------------------------------------------------------------
844+
845+
const FROM_CONFIG_INITIAL: MonkeyTableConfig = {
846+
schemaVersion: 1,
847+
columns: [
848+
{ id: 'Name' },
849+
{ id: 'Email', type: 'Email' },
850+
{ id: 'Role', type: 'SingleSelect', options: {
851+
options: [
852+
{ value: 'Engineer', label: 'Engineer', color: '#dbeafe' },
853+
{ value: 'Designer', label: 'Designer', color: '#fce7f3' },
854+
{ value: 'Manager', label: 'Manager', color: '#dcfce7' },
855+
],
856+
} },
857+
{ id: 'Salary', type: 'Number', options: { format: 'currency', precision: 0 } },
858+
{ id: 'Joined', type: 'Date', options: { dateFormat: 'iso' } },
859+
{ id: 'Active', type: 'Boolean' },
860+
{ id: 'Website', type: 'URL' },
861+
],
862+
rows: [
863+
{ Name: 'Alice Chen', Email: 'alice@acme.com', Role: 'Engineer', Salary: 95000, Joined: '2022-03-15', Active: true, Website: 'https://alicechen.dev' },
864+
{ Name: 'Bob Smith', Email: 'bob@acme.com', Role: 'Designer', Salary: 82000, Joined: '2021-07-01', Active: true, Website: 'https://bobsmith.design' },
865+
{ Name: 'Carol Wu', Email: 'carol@acme.com', Role: 'Manager', Salary: 110000, Joined: '2020-11-20', Active: false, Website: 'https://carolwu.io' },
866+
],
867+
settings: {
868+
editable: true,
869+
height: 'auto',
870+
rowHeight: 'medium',
871+
pageSize: 25,
872+
locale: 'en-US',
873+
currencyCode: 'USD',
874+
showRowNumbers: true,
875+
},
876+
};
877+
878+
function FromConfigExample({ locale, language, compactMode }: { locale: string; language: string; compactMode: boolean }) {
879+
const [config, setConfig] = useState<MonkeyTableConfig>(FROM_CONFIG_INITIAL);
880+
const [configText, setConfigText] = useState(() => JSON.stringify(FROM_CONFIG_INITIAL, null, 2));
881+
const [parseError, setParseError] = useState<string | null>(null);
882+
883+
// Merge the tab-level locale/compact controls onto the config's own settings
884+
// so the top-bar switches still work for this tab.
885+
const effectiveConfig = useMemo<MonkeyTableConfig>(() => ({
886+
...config,
887+
settings: {
888+
...config.settings,
889+
locale,
890+
language,
891+
compactMode,
892+
},
893+
}), [config, locale, language, compactMode]);
894+
895+
const handleConfigChange = (text: string) => {
896+
setConfigText(text);
897+
try {
898+
const parsed = JSON.parse(text) as MonkeyTableConfig;
899+
setConfig(parsed);
900+
setParseError(null);
901+
} catch (err) {
902+
setParseError(err instanceof Error ? err.message : String(err));
903+
}
904+
};
905+
906+
const handleRowsChange = (rows: Array<Record<string, Value>>) => {
907+
// Reflect live edits back into the config so the JSON pane stays in sync.
908+
setConfig((prev) => ({ ...prev, rows }));
909+
setConfigText(JSON.stringify({ ...config, rows }, null, 2));
910+
};
911+
912+
return (
913+
<div className="example">
914+
<h2>From Config</h2>
915+
<div className="desc">
916+
One <code>MonkeyTableConfig</code> blob — <code>{`{ columns, rows, settings }`}</code> — rendered by{' '}
917+
<code>&lt;MonkeyTableFromConfig&gt;</code>. Edit the JSON on the right and the table re-renders live.
918+
Edit cells in the table and the JSON updates to match.
919+
</div>
920+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 440px', gap: 16, alignItems: 'stretch' }}>
921+
<div className="table-container" style={{ height: 420 }}>
922+
<MonkeyTableFromConfig
923+
config={effectiveConfig}
924+
onChange={handleRowsChange}
925+
/>
926+
</div>
927+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
928+
<div style={{ fontSize: 12, fontWeight: 600, color: '#374151' }}>config (live JSON)</div>
929+
<textarea
930+
value={configText}
931+
onChange={(e) => handleConfigChange(e.target.value)}
932+
spellCheck={false}
933+
style={{
934+
flex: 1,
935+
minHeight: 380,
936+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
937+
fontSize: 12,
938+
lineHeight: 1.5,
939+
padding: 10,
940+
borderRadius: 6,
941+
border: `1px solid ${parseError ? '#ef4444' : '#d1d5db'}`,
942+
background: '#fafafa',
943+
color: '#111827',
944+
resize: 'vertical',
945+
}}
946+
/>
947+
{parseError && (
948+
<div style={{ fontSize: 12, color: '#b91c1c' }}>
949+
JSON parse error: {parseError} — table still shows the last valid config.
950+
</div>
951+
)}
952+
</div>
953+
</div>
954+
</div>
955+
);
956+
}
957+
836958
const TABS: Array<{ id: string; label: string; private?: boolean }> = [
837959
{ id: 'editable', label: 'Editable' },
960+
{ id: 'from-config', label: 'From Config' },
838961
{ id: 'async-crud', label: 'Async CRUD' },
839962
{ id: 'multiplayer', label: 'Multiplayer' },
840963
{ id: 'height', label: 'Height Modes' },
@@ -1011,6 +1134,10 @@ function App() {
10111134
</div>
10121135
)}
10131136

1137+
{tab === 'from-config' && (
1138+
<FromConfigExample locale={locale} language={language} compactMode={compactMode} />
1139+
)}
1140+
10141141
{tab === 'height' && (
10151142
<HeightModesExample locale={locale} language={language} compactMode={compactMode} />
10161143
)}

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.4.0",
3+
"version": "0.5.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.4.0",
3+
"version": "0.5.0",
44
"description": "Embeddable, editable React table component",
55
"keywords": [
66
"react",
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* MonkeyTableFromConfig — façade that accepts a flat JSON config and forwards
3+
* everything non-serializable (handlers, presence, custom renderers, ref) as
4+
* ordinary React props.
5+
*
6+
* <MonkeyTableFromConfig
7+
* config={{ columns, rows, settings }}
8+
* onChange={handle}
9+
* />
10+
*/
11+
import { forwardRef } from 'react';
12+
import type { ForwardedRef } from 'react';
13+
import { MonkeyTable, type MonkeyTableHandle, type MonkeyTableProps } from './MonkeyTable.tsx';
14+
import { resolveConfig, type MonkeyTableConfig } from './config.ts';
15+
16+
/**
17+
* Anything that can't live in JSON: React handlers, presence data, custom
18+
* function/constraint/renderer/editor registrations, selection actions,
19+
* upload hook, column lifecycle callbacks. These stay as component props.
20+
*/
21+
export type MonkeyTableRuntimeProps = Pick<MonkeyTableProps,
22+
| 'onChange' | 'onRowsChange' | 'onSelectionChange' | 'onRowClick'
23+
| 'onCellChange' | 'onActiveCellChange'
24+
| 'onRowCreate' | 'onCellSave' | 'onRowDelete' | 'onHookError'
25+
| 'onGroupByChange' | 'onColorByChange' | 'onSortChange' | 'onPageChange'
26+
| 'onColumnRename' | 'onColumnDelete' | 'onColumnCreate'
27+
| 'onColumnChangeType' | 'onColumnUpdateOptions'
28+
| 'onUpload'
29+
| 'rowKey'
30+
| 'selectionActions'
31+
| 'presence'
32+
| 'functions' | 'constraints' | 'renderers' | 'editors'
33+
>;
34+
35+
export interface MonkeyTableFromConfigProps extends MonkeyTableRuntimeProps {
36+
config: MonkeyTableConfig;
37+
}
38+
39+
function MonkeyTableFromConfigInner(
40+
{ config, ...runtime }: MonkeyTableFromConfigProps,
41+
ref: ForwardedRef<MonkeyTableHandle>,
42+
) {
43+
const resolved = resolveConfig(config);
44+
// Runtime `rowKey` (including the function form) takes precedence over any
45+
// string in the config — lets consumers escalate from JSON to code gradually.
46+
return <MonkeyTable ref={ref} {...resolved} {...runtime} />;
47+
}
48+
49+
export const MonkeyTableFromConfig = forwardRef<MonkeyTableHandle, MonkeyTableFromConfigProps>(
50+
MonkeyTableFromConfigInner,
51+
);
52+
MonkeyTableFromConfig.displayName = 'MonkeyTableFromConfig';

0 commit comments

Comments
 (0)