@updog/data-editor-wc 0.1.4 → 0.1.6

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 CHANGED
@@ -1,11 +1,15 @@
1
1
  # @updog/data-editor-wc
2
2
 
3
- Web Component wrapper for the Updog Data Editor. Framework-agnostic works with vanilla JS, Angular, Vue, or any framework.
3
+ Client-side data importer and spreadsheet editor SDK, shipped as a Web Component. Your users import files, match columns to your schema, fix errors, and submit clean data. Edits happen inline, in the browser, at 1M+ rows.
4
+
5
+ Framework-agnostic — works with vanilla JS, Angular, Vue, Svelte, or any framework that renders HTML.
6
+
7
+ > **Using React?** Use the first-class React SDK instead — [`@updog/data-editor`](https://www.npmjs.com/package/@updog/data-editor) — for typed props, hooks-friendly integration, and a smaller bundle (shares your app's React).
4
8
 
5
9
  ## Requirements
6
10
 
7
- - An API key — this is a **commercial SDK**. Sign up at [updog.tech](https://updog.tech) to get one. The editor will not render without it.
8
11
  - Any modern evergreen browser (Chrome, Firefox, Safari, Edge).
12
+ - An API key — this is a **commercial SDK**. Sign up at [updog.tech](https://updog.tech) to get one.
9
13
 
10
14
  ## Installation
11
15
 
@@ -13,205 +17,427 @@ Web Component wrapper for the Updog Data Editor. Framework-agnostic — works wi
13
17
  npm install @updog/data-editor-wc
14
18
  ```
15
19
 
16
- ## Usage
20
+ ## Register
21
+
22
+ ```js
23
+ import "@updog/data-editor-wc";
24
+ import "@updog/data-editor-wc/styles.css";
25
+ ```
26
+
27
+ Defines `<updog-editor>` globally. Import once, typically at your app entry.
28
+
29
+ How you wire the element into a framework (Angular's `CUSTOM_ELEMENTS_SCHEMA`, Vue's `compilerOptions.isCustomElement`, Svelte's `svelte:element`, etc.) depends on your stack. Once registered, the rest of this guide is the same.
30
+
31
+ ## Quick Start
17
32
 
18
33
  ```html
34
+ <button id="open-btn">Open Editor</button>
35
+ <updog-editor id="editor"></updog-editor>
36
+
19
37
  <script type="module">
20
38
  import "@updog/data-editor-wc";
21
39
  import "@updog/data-editor-wc/styles.css";
40
+ import { required, email } from "@updog/data-editor-wc";
41
+
42
+ const editor = document.getElementById("editor");
43
+
44
+ editor.configure({
45
+ apiKey: "your-license-key",
46
+ primaryKey: "id",
47
+ columns: [
48
+ { id: "name", title: "Full Name", size: 200, validate: required("Name is required") },
49
+ { id: "email", title: "Email", size: 250, validate: [required("Email is required"), email("Invalid email")] },
50
+ { id: "role", title: "Role", editor: { type: "select", options: ["Admin", "Editor", "Viewer"] } },
51
+ ],
52
+ loadData: async (onChunk) => {
53
+ const res = await fetch("/api/employees");
54
+ onChunk(await res.json());
55
+ },
56
+ onComplete: async (result, actions) => {
57
+ for (const source of result.sources) {
58
+ const inserts = source.rows.filter((r) => r.isNew && !r.isDeleted && r.isValid);
59
+ const updates = source.rows.filter((r) => !r.isNew && r.isChanged && !r.isDeleted && r.isValid);
60
+ const deletes = source.rows.filter((r) => r.isDeleted && !r.isNew);
61
+ await persist(source.sourceId, { inserts, updates, deletes });
62
+ }
63
+ actions.reset();
64
+ editor.hide();
65
+ },
66
+ });
22
67
 
23
- const editor = document.querySelector("updog-editor");
68
+ document.getElementById("open-btn").addEventListener("click", () => editor.show());
69
+ editor.addEventListener("close", () => editor.hide());
70
+ </script>
71
+ ```
24
72
 
25
- editor.columns = [
26
- { id: "name", title: "Name", width: 150 },
27
- { id: "email", title: "Email", width: 260 },
28
- ];
29
- editor.primaryKey = "id";
30
- editor.apiKey = "your-api-key";
73
+ ## Setting Props
31
74
 
32
- editor.loadData = async (onChunk) => {
33
- const res = await fetch("/api/data");
34
- onChunk(await res.json());
35
- };
75
+ Two ways to configure the element: HTML attributes for primitives, JS properties for everything else.
36
76
 
37
- editor.onComplete = (result) => {
38
- console.log("Submitted:", result);
39
- editor.hide();
40
- };
77
+ ### HTML attributes
41
78
 
42
- editor.addEventListener("close", () => editor.hide());
43
- document.getElementById("open-btn").addEventListener("click", () => editor.show());
44
- </script>
79
+ Use for primitive values. The element observes these and re-renders on change.
45
80
 
46
- <button id="open-btn">Open Editor</button>
47
- <updog-editor></updog-editor>
48
- ```
81
+ | Attribute | Maps to | Type |
82
+ |---|---|---|
83
+ | `api-key` | `apiKey` | string |
84
+ | `primary-key` | `primaryKey` | string |
85
+ | `locale` | `locale` | string |
86
+ | `variant` | `variant` | `"editor"` \| `"uploader"` |
87
+ | `mode` | `mode` | `"modal"` \| `"inline"` |
88
+ | `open` | `open` | boolean (presence = true) |
89
+ | `rtl` | `rtl` | boolean (presence = true) |
90
+ | `readonly` | `readonly` | boolean (presence = true) |
49
91
 
50
- ## HTML Attributes
92
+ ```html
93
+ <updog-editor api-key="your-key" primary-key="id" variant="uploader" readonly></updog-editor>
94
+ ```
51
95
 
52
- Simple values settable directly in HTML:
96
+ ### JS properties
53
97
 
54
- | Attribute | Type | Description |
55
- |---|---|---|
56
- | `api-key` | string | License key |
57
- | `primary-key` | string | Row identifier column |
58
- | `locale` | string | BCP 47 locale tag (default: `"en"`) |
59
- | `variant` | `"editor"` \| `"uploader"` | UI mode |
60
- | `mode` | `"modal"` \| `"inline"` | Rendering mode (default: `"modal"`) |
61
- | `open` | boolean (presence) | Whether the editor modal is open (modal mode only) |
62
- | `rtl` | boolean (presence) | Right-to-left layout |
63
- | `readonly` | boolean (presence) | Hide write-oriented UI |
98
+ Everything else objects, functions, arrays — must be set as a JS property or via `configure()`.
64
99
 
65
- ## JS Properties
100
+ ```js
101
+ const editor = document.querySelector("updog-editor");
102
+ editor.columns = [...];
103
+ editor.loadData = async (onChunk) => { ... };
104
+ editor.onComplete = (result, actions) => { ... };
105
+ ```
66
106
 
67
- Complex values set via JavaScript:
107
+ ### `configure(props)`
68
108
 
69
- | Property | Type |
70
- |---|---|
71
- | `columns` | `DataEditorColumn[]` |
72
- | `loadData` | `(onChunk: (rows) => void) => Promise<void>` |
73
- | `onComplete` | `(result, actions) => void` |
74
- | `translations` | `DataEditorTranslations` |
75
- | `importFormats` | `("csv" \| "tsv" \| "xlsx" \| "json" \| "xml")[]` |
76
- | `exportFormats` | `("csv" \| "tsv" \| "xlsx" \| "json" \| "xml")[]` |
77
- | `enableDeleteRow` | `"all"` \| `"new"` \| `false` |
78
- | `enableAddRow` | `boolean` |
79
- | `localStorage` | `false \| { licenseGrant?: boolean }` |
80
- | `rowHeight` | `number` (default: 34) |
81
- | `headerHeight` | `number` (default: 36) |
82
-
83
- All properties can also be set in bulk via `configure()`:
109
+ Set multiple properties at once. Useful during setup. Accepts every prop except `onClose`. Property updates are batched — the editor re-renders once per microtask regardless of how many properties you set.
84
110
 
85
111
  ```js
86
112
  editor.configure({
87
- columns: [...],
113
+ apiKey: "your-key",
88
114
  primaryKey: "id",
89
- loadData: myLoadFn,
115
+ columns: [...],
116
+ loadData: async (onChunk) => onChunk(await fetchRows()),
117
+ onComplete: (result, actions) => { save(result); actions.reset(); },
90
118
  });
91
119
  ```
92
120
 
121
+ ## Inline Mode
122
+
123
+ Render the editor directly in the DOM without a modal overlay:
124
+
125
+ ```html
126
+ <updog-editor mode="inline" api-key="your-key"></updog-editor>
127
+ ```
128
+
129
+ In inline mode, `show()` / `hide()` and the `open` attribute don't apply. The `close` event is not fired.
130
+
131
+ ## Props
132
+
133
+ | Prop | Type | Required | Default | Description |
134
+ |---|---|---|---|---|
135
+ | `apiKey` | `string` | Yes | — | Your Updog license key. Validated on each open. |
136
+ | `columns` | `DataEditorColumn[]` | Yes | — | Column definitions. |
137
+ | `primaryKey` | `string` | Yes | — | Column ID that uniquely identifies each row. |
138
+ | `mode` | `"modal"` \| `"inline"` | No | `"modal"` | Rendering mode. |
139
+ | `open` | `boolean` | Modal only | — | Controlled modal visibility. Equivalent to `show()` / `hide()`. |
140
+ | `loadData` | `(onChunk) => Promise<void>` | No | — | Async data loader. Stream rows in chunks, optionally tagged by source. |
141
+ | `onComplete` | `(result, actions) => void` | No | — | Called on submit. See [Submission Result](#submission-result). |
142
+ | `variant` | `"editor"` \| `"uploader"` | No | `"editor"` | Initial view. `"uploader"` opens the import wizard first. |
143
+ | `translations` | `DataEditorTranslations` | No | — | Partial i18n overrides. |
144
+ | `locale` | `string` | No | `"en"` | BCP 47 locale tag. |
145
+ | `rtl` | `boolean` | No | `false` | Right-to-left layout. |
146
+ | `readonly` | `boolean` | No | `false` | Hide all editing UI. |
147
+ | `enableDeleteRow` | `"all"` \| `"new"` \| `false` | No | `false` | Row deletion policy. |
148
+ | `enableAddRow` | `boolean` | No | `true` | Show the "Add row" button. |
149
+ | `enableCreateColumn` | `boolean` | No | `true` | Allow creating columns for unmatched CSV headers during import. |
150
+ | `importFormats` | `DataEditorFormat[]` | No | all | Allowed import formats. `[]` disables import. |
151
+ | `exportFormats` | `DataEditorFormat[]` | No | all | Allowed export formats. `[]` disables export. |
152
+ | `remoteSources` | `RemoteSource[]` | No | — | Custom import buttons (Google Sheets, S3, etc.) rendered on the upload step. |
153
+ | `rowHeight` | `number` | No | `34` | Row height in pixels. |
154
+ | `headerHeight` | `number` | No | `36` | Header height in pixels. |
155
+ | `server` | `DataEditorServer` | No | — | Server-delegated mode: SDK renders, your backend handles queries and mutations. |
156
+ | `chat` | `DataEditorChat` | No | — | Bring-your-own AI chat panel. |
157
+ | `onColumnMatch` | `(headers, columns) => ...` | No | — | Override import column matching. |
158
+ | `onValueMatch` | `(valuesToMatch) => ...` | No | — | Override import value matching for `select` columns. |
159
+ | `synonyms` | `Record<string, string[]>` | No | — | Extra synonyms for column auto-matching. |
160
+ | `sampleData` | `Record<string, unknown>[]` | No | — | Rows used in the "Download Example" file. |
161
+ | `localStorage` | `false` \| `{ licenseGrant?: boolean }` | No | `{ licenseGrant: true }` | What the SDK caches in `localStorage`. |
162
+ | `onError` | `(error: UpdogError) => void` | No | — | Called on internal errors. Use for logging or Sentry. |
163
+ | `className` | `string` | No | — | CSS class on the wrapper element. |
164
+
93
165
  ## Methods
94
166
 
95
167
  | Method | Description |
96
168
  |---|---|
97
- | `show()` | Opens the editor modal |
98
- | `hide()` | Closes the editor modal |
99
- | `configure(props)` | Bulk-set any props |
169
+ | `show()` | Opens the editor modal. Equivalent to setting `open` to `true`. No-op in `mode="inline"`. |
170
+ | `hide()` | Closes the editor modal. Equivalent to setting `open` to `false`. No-op in `mode="inline"`. |
171
+ | `configure(props)` | Bulk-set any props. See [`configure(props)`](#configureprops). |
100
172
 
101
173
  ## Events
102
174
 
103
- | Event | Description |
104
- |---|---|
105
- | `close` | Fired when the user closes the editor from inside (X button, discard). Call `editor.hide()` in response. |
175
+ ### `close`
106
176
 
107
- ## Angular
177
+ Fires when the user closes the modal (X button or Escape key). Replaces the React `onClose` prop — the WC dispatches a bubbling `CustomEvent` instead. Not fired in `mode="inline"`.
108
178
 
109
- ```ts
110
- // app.module.ts
111
- @NgModule({ schemas: [CUSTOM_ELEMENTS_SCHEMA] })
179
+ ```js
180
+ editor.addEventListener("close", () => editor.hide());
181
+ ```
112
182
 
113
- // main.ts
114
- import "@updog/data-editor-wc";
115
- import "@updog/data-editor-wc/styles.css";
183
+ ## Column Configuration
184
+
185
+ ```js
186
+ import { required, email } from "@updog/data-editor-wc";
187
+
188
+ editor.columns = [
189
+ {
190
+ id: "email",
191
+ title: "Email",
192
+ size: 260,
193
+
194
+ // One validator, or an array. Each returns `{ level: "error", message }` or null.
195
+ validate: [required("Email is required"), email("Enter a valid email")],
196
+
197
+ // Flag duplicates in this column as errors.
198
+ unique: true,
199
+
200
+ // Revalidate these columns when this one changes.
201
+ dependentFields: ["confirmEmail"],
202
+
203
+ // Cell editor. Default is "text".
204
+ editor: { type: "select", options: ["US", "UK", "DE"] },
205
+
206
+ // Display-only formatting. Does not mutate stored data.
207
+ formatter: (value) => value.toLowerCase(),
208
+
209
+ // Runs when rows are uploaded to the editor. Mutates the stored value.
210
+ transformer: (value) => String(value).trim(),
211
+
212
+ // Sidebar filter control.
213
+ filter: { type: "select" },
214
+
215
+ // Lock cells in this column. `"default"` locks only default-source rows.
216
+ locked: "default",
217
+ },
218
+ ];
116
219
  ```
117
220
 
118
- ```html
119
- <updog-editor api-key="xxx" #editor (close)="onClose()"></updog-editor>
221
+ **Cell editors**: `{ type: "text" }` (default), `{ type: "date", minDate?, maxDate? }`, `{ type: "select", options: string[] }`, `{ type: "number", decimalPlaces?, decimalSeparator?, thousandsSeparator?, allowChars? }`.
222
+
223
+ **Column filters**: `{ type: "select", label?, placeholder?, options?, multiple? }`, `{ type: "number-range", label? }`, `{ type: "date-range", label? }`.
224
+
225
+ ## Built-in Validators
226
+
227
+ Validators are factory functions — call each one with the error message you want displayed.
228
+
229
+ ```js
230
+ import {
231
+ required,
232
+ numeric,
233
+ email,
234
+ date,
235
+ oneOf,
236
+ endDateAfterStart,
237
+ } from "@updog/data-editor-wc";
238
+
239
+ editor.columns = [
240
+ { id: "name", title: "Name", validate: required("Required") },
241
+ { id: "email", title: "Email", validate: [required("Required"), email("Invalid email")] },
242
+ { id: "salary", title: "Salary", validate: numeric("Must be a number") },
243
+ { id: "status", title: "Status", validate: oneOf(["active", "inactive"], "Invalid status") },
244
+ {
245
+ id: "startDate",
246
+ title: "Start",
247
+ validate: date("Use YYYY-MM-DD or DD/MM/YYYY"),
248
+ editor: { type: "date" },
249
+ dependentFields: ["endDate"],
250
+ },
251
+ {
252
+ id: "endDate",
253
+ title: "End",
254
+ validate: [date("Invalid date"), endDateAfterStart("startDate", "End must be on or after start")],
255
+ editor: { type: "date" },
256
+ },
257
+ ];
120
258
  ```
121
259
 
122
- ```ts
123
- @ViewChild("editor") editorRef: ElementRef;
124
-
125
- ngOnInit() {
126
- const el = this.editorRef.nativeElement;
127
- el.columns = this.columns;
128
- el.primaryKey = "id";
129
- el.loadData = this.loadData;
130
- el.onComplete = this.handleComplete;
131
- }
260
+ A `ValidationError` with `level: "error"` flags the cell in the grid but does **not** block submission — invalid rows are delivered to `onComplete` tagged via `isValid: false`.
261
+
262
+ ### Custom validators
263
+
264
+ A validator is `(value, row) => ValidationError | null`. The only `level` is `"error"`.
265
+
266
+ ```js
267
+ const mustContainAt = (value) => {
268
+ if (!value) return null;
269
+ if (!String(value).includes("@")) return { level: "error", message: "Must contain @" };
270
+ return null;
271
+ };
272
+ ```
273
+
274
+ ## Data Loading
275
+
276
+ `loadData` is called once when the editor opens. Call `onChunk` one or more times to stream rows. The grid renders each chunk without blocking.
277
+
278
+ ```js
279
+ // Single source
280
+ editor.loadData = async (onChunk) => {
281
+ for (let page = 0; page < totalPages; page++) {
282
+ const rows = await fetch(`/api/employees?page=${page}`).then((r) => r.json());
283
+ onChunk(rows);
284
+ }
285
+ };
286
+
287
+ // Multiple sources — each chunk tagged with a source
288
+ editor.loadData = async (onChunk) => {
289
+ onChunk(await fetchSalesforce(), { source: "Salesforce", done: true });
290
+ onChunk(await fetchHubSpot(), { source: "HubSpot", deletable: true, done: true });
291
+ };
132
292
  ```
133
293
 
134
- ## Vue
294
+ `ChunkSourceOptions`: `{ source: string; id?: string; deletable?: boolean; done?: boolean }`. Sources are auto-registered on first encounter. Chunks without options go to "Existing Data". When `loadData` resolves, any source still loading is finalized automatically.
295
+
296
+ ## Submission Result
297
+
298
+ `onComplete(result, actions)` fires when the user submits.
135
299
 
136
300
  ```ts
137
- import "@updog/data-editor-wc";
138
- import "@updog/data-editor-wc/styles.css";
301
+ type DataEditorResult = {
302
+ sources: {
303
+ sourceId: string;
304
+ sourceName: string;
305
+ rows: {
306
+ row: Record<string, unknown>;
307
+ isNew: boolean; // imported or manually added this session
308
+ isChanged: boolean; // cell values differ from origin
309
+ isDeleted: boolean; // marked for deletion
310
+ isValid: boolean; // passes all validators
311
+ }[];
312
+ }[];
313
+ counts: { new: number; changed: number; deleted: number; invalid: number };
314
+ };
315
+
316
+ type DataEditorActions = {
317
+ reset: () => void; // discard changes and reload via loadData
318
+ };
319
+ ```
320
+
321
+ Pristine backend rows (unchanged, not deleted, not new) are omitted from `sources[].rows` — they're no-ops. The flags are orthogonal: a row can be new, changed, and deleted at once. You own the routing rules:
322
+
323
+ ```js
324
+ editor.onComplete = async (result, actions) => {
325
+ for (const source of result.sources) {
326
+ const inserts = source.rows.filter((r) => r.isNew && !r.isDeleted && r.isValid);
327
+ const updates = source.rows.filter((r) => !r.isNew && r.isChanged && !r.isDeleted && r.isValid);
328
+ const deletes = source.rows.filter((r) => r.isDeleted && !r.isNew);
329
+ await persist(source.sourceId, { inserts, updates, deletes });
330
+ }
331
+ actions.reset();
332
+ };
139
333
  ```
140
334
 
141
- ```vue
142
- <template>
143
- <updog-editor ref="editor" api-key="xxx" @close="editor.hide()" />
144
- </template>
335
+ If you never tagged sources via `loadData`, you'll get one entry with `sourceId: "backend"`. `result.counts` is an aggregate for summary UI — route with the per-row flags, not counts.
145
336
 
146
- <script setup>
147
- import { ref, onMounted } from "vue";
337
+ ## Import Configuration
148
338
 
149
- const editor = ref();
339
+ The import wizard guides users through uploading a file, mapping columns to your schema, and resolving value mismatches.
150
340
 
151
- onMounted(() => {
152
- editor.value.columns = columns;
153
- editor.value.primaryKey = "id";
154
- editor.value.loadData = loadData;
155
- editor.value.onComplete = handleComplete;
156
- });
157
- </script>
341
+ ### Formats
342
+
343
+ Supported: `"csv"`, `"tsv"`, `"xlsx"`, `"json"`, `"xml"`.
344
+
345
+ ```js
346
+ editor.importFormats = ["csv", "xlsx"]; // only CSV and XLSX
347
+ editor.exportFormats = []; // disable export entirely
158
348
  ```
159
349
 
160
- ## Column Configuration
350
+ ### Override column matching
161
351
 
162
352
  ```js
163
- editor.columns = [
353
+ editor.onColumnMatch = async (headers, columns) => {
354
+ const mappings = await myMatchingService.match(headers, columns);
355
+ return mappings; // { csvHeader: columnId | null }
356
+ };
357
+ ```
358
+
359
+ Return a map of `{ csvHeader: columnId | null }`. Entries set to `null` or omitted fall back to built-in matching.
360
+
361
+ ### Override value matching
362
+
363
+ For select columns, override imported-value → option mapping:
364
+
365
+ ```js
366
+ editor.onValueMatch = async (valuesToMatch) => {
367
+ // valuesToMatch = { country: { importedValues: ["espana", "fr"], options: ["Spain", "France"] } }
368
+ return {
369
+ country: { espana: "Spain", fr: "France" },
370
+ };
371
+ };
372
+ ```
373
+
374
+ Values set to `null` skip auto-matching. Unmapped values fall back to built-in fuzzy matching.
375
+
376
+ ### Synonyms
377
+
378
+ Extra aliases for the built-in column matcher:
379
+
380
+ ```js
381
+ editor.synonyms = {
382
+ productSku: ["sku", "article_no", "item_code"],
383
+ firstName: ["first", "given_name", "fname"],
384
+ };
385
+ ```
386
+
387
+ ### Remote sources
388
+
389
+ Render custom import buttons for third-party sources. You own auth and picker logic; the SDK renders the button and processes the result.
390
+
391
+ ```js
392
+ editor.remoteSources = [
164
393
  {
165
- id: "email",
166
- title: "Email",
167
- width: 260,
168
- // Validation
169
- validate: (value) => {
170
- if (!value) return { level: "error", message: "Required" };
171
- return null;
394
+ id: "google-sheets",
395
+ label: "Google Sheets",
396
+ icon: "<svg>...</svg>",
397
+ fetch: async () => {
398
+ const data = await myGoogleSheetsLib.pick();
399
+ return data.rows; // Record<string, unknown>[] or return a File
172
400
  },
173
- unique: true,
174
- // Cell editor type: "text" (default), "date", or "select"
175
- editor: { type: "text" },
176
- // Visual formatting (display only)
177
- formatter: (value) => value.toLowerCase(),
178
- // Data transformation on input
179
- transformer: (value) => String(value).trim(),
180
- // Filter panel configuration
181
- filter: { type: "select" },
182
401
  },
183
402
  ];
184
403
  ```
185
404
 
186
- ## Inline Mode
405
+ Return a `File` to parse via the standard CSV/XLSX/JSON/XML pipeline, or return structured records to skip parsing.
187
406
 
188
- Render the editor directly in the DOM without a modal overlay:
407
+ ## Error Handling
189
408
 
190
- ```html
191
- <updog-editor mode="inline" api-key="xxx"></updog-editor>
409
+ Log SDK-internal errors through `onError`. The SDK recovers gracefully — use this for monitoring, not recovery.
410
+
411
+ ```js
412
+ editor.onError = (error) => {
413
+ Sentry.captureException(error.originalError ?? error, {
414
+ tags: { code: error.code, source: error.source },
415
+ });
416
+ };
192
417
  ```
193
418
 
194
- In inline mode, `show()`/`hide()` and the `open` attribute are no-ops. The editor renders in-place immediately.
419
+ Error codes: `PARSE_ERROR`, `RENDER_ERROR`, `TRANSFORM_ERROR`, `VALIDATION_ERROR`, `WORKER_ERROR`, `COMMAND_ERROR`, `OPERATION_ERROR`.
195
420
 
196
421
  ## Styling
197
422
 
198
- `<updog-editor>` renders in **light DOM by design** — no Shadow DOM isolation. This is intentional: you can override any editor style with your own CSS targeting descendants of `updog-editor`.
423
+ `<updog-editor>` renders in **light DOM by design** — no Shadow DOM isolation. This is intentional: you can override any editor style with CSS targeting descendants of `updog-editor`.
199
424
 
200
425
  ### CSS variables
201
426
 
202
- The editor reads colors, spacing, and typography from CSS custom properties. Override them in your page's CSS:
427
+ The editor reads colors from CSS custom properties. Override them on `:root` or on the element itself:
203
428
 
204
429
  ```css
205
430
  updog-editor {
206
- --updog-color-primary: #0070f3;
207
- --updog-font-family: "Inter", system-ui, sans-serif;
208
- --updog-radius-md: 8px;
431
+ --updog-grid-cell-bg-idle: #ffffff;
432
+ --updog-grid-header-bg-idle: #f8f9fa;
209
433
  }
210
434
  ```
211
435
 
436
+ See the [Styling reference](https://docs.updog.tech/styling/) for the full variable list.
437
+
212
438
  ### Class names
213
439
 
214
- Internal elements use BEM-style class names prefixed with `updog__`. Target them directly for deep customization:
440
+ Internal elements use BEM-style class names prefixed with `updog__`. Target them for deep customization:
215
441
 
216
442
  ```css
217
443
  updog-editor .updog__button--primary { background: purple; }
@@ -228,9 +454,19 @@ body *:not(updog-editor *) { /* your global rules */ }
228
454
  ## Notes
229
455
 
230
456
  - Fonts are inherited from your page. The editor uses `var(--updog-font-family)` which defaults to system fonts.
231
- - In modal mode, the editor portals to `document.body` as a full-screen overlay. In inline mode, it renders in-place with no overlay.
457
+ - In modal mode, the editor portals to `document.body` as a full-screen overlay. In inline mode, it renders in place with no overlay.
232
458
  - Multiple `<updog-editor>` instances on the same page are fully independent.
233
459
 
460
+ ## Using React?
461
+
462
+ Use the first-class React SDK instead:
463
+
464
+ ```bash
465
+ npm install @updog/data-editor
466
+ ```
467
+
468
+ See [@updog/data-editor](https://www.npmjs.com/package/@updog/data-editor).
469
+
234
470
  ## License
235
471
 
236
- Commercial — see [LICENSE](./LICENSE) for the full terms of the Updog SDK Commercial License v1.0. Contact `admin@updog.tech` for enterprise licensing questions. Third-party dependencies bundled with this package are listed in `THIRD_PARTY_NOTICES.txt`.
472
+ Commercial — see [LICENSE](./LICENSE) for the full terms of the Updog SDK Commercial License v1.0. Contact `admin@updog.tech` for enterprise licensing. Third-party dependencies bundled with this package are listed in `THIRD_PARTY_NOTICES.txt`.
@@ -0,0 +1 @@
1
+ (function(){function e(e){let{ops:t,rows:n,fields:r}=e,i={};for(let[t,n]of Object.entries(e.ctx.opts))i[t]=new Set(n);let a={opts:i},o=[],s=[];for(let e of t)try{let t=Function(`return `+e.fn)();s.push({action:e.action,fn:t})}catch(t){o.push(`Failed to compile ${e.action} op: ${t instanceof Error?t.message:String(t)}`)}let c=new Map,l=[];for(let[e,t]of n){let n={...t},i=!1;for(let e of s)if(e.action===`delete`)try{if(e.fn(n,a)){i=!0;break}}catch{}else try{e.fn(n,a)}catch{}if(i){l.push(e);continue}for(let i of r){let r=t[i],a=n[i];if(a!==r){let t=c.get(i);t||(t=[],c.set(i,t)),t.push([e,r,a])}}}let u=[];for(let[e,t]of c)u.push({field:e,changes:t});return{deltas:u,deleteIds:l,errors:o}}let t=self;self.addEventListener(`message`,n=>{try{let{ops:r,ctx:i,rows:a,fields:o}=n.data,s=e({ops:r,ctx:i,rows:a,fields:o});t.postMessage({type:`RESULT`,deltas:s.deltas,deleteIds:s.deleteIds,errors:s.errors})}catch(e){t.postMessage({type:`ERROR`,message:e instanceof Error?e.message:`Transform worker error`})}})})();