@updog/data-editor-wc 0.1.0

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/index.d.ts ADDED
@@ -0,0 +1,1593 @@
1
+ declare var export_default = {
2
+ ui: {
3
+ button: {
4
+ defaultAriaLabel: "Button",
5
+ },
6
+ calendar: {
7
+ previousMonth: "Previous month",
8
+ nextMonth: "Next month",
9
+ months: {
10
+ january: "January",
11
+ february: "February",
12
+ march: "March",
13
+ april: "April",
14
+ may: "May",
15
+ june: "June",
16
+ july: "July",
17
+ august: "August",
18
+ september: "September",
19
+ october: "October",
20
+ november: "November",
21
+ december: "December",
22
+ },
23
+ monthsShort: {
24
+ january: "Jan",
25
+ february: "Feb",
26
+ march: "Mar",
27
+ april: "Apr",
28
+ may: "May",
29
+ june: "Jun",
30
+ july: "Jul",
31
+ august: "Aug",
32
+ september: "Sep",
33
+ october: "Oct",
34
+ november: "Nov",
35
+ december: "Dec",
36
+ },
37
+ weekDays: {
38
+ mo: "Mo",
39
+ tu: "Tu",
40
+ we: "We",
41
+ th: "Th",
42
+ fr: "Fr",
43
+ sa: "Sa",
44
+ su: "Su",
45
+ },
46
+ weekDaysFull: {
47
+ mo: "Monday",
48
+ tu: "Tuesday",
49
+ we: "Wednesday",
50
+ th: "Thursday",
51
+ fr: "Friday",
52
+ sa: "Saturday",
53
+ su: "Sunday",
54
+ },
55
+ label: "Calendar",
56
+ selectMonth: "{{month}}, select month",
57
+ selectYear: "{{year}}, select year",
58
+ },
59
+ datePicker: {
60
+ placeholder: "DD/MM/YYYY",
61
+ openCalendar: "Open calendar",
62
+ clearSelection: "Clear selection",
63
+ },
64
+ input: {
65
+ clearField: "Clear field",
66
+ showPassword: "Show password",
67
+ hidePassword: "Hide password",
68
+ },
69
+ select: {
70
+ noResults: "No results",
71
+ clearSelection: "Clear selection",
72
+ },
73
+ comboBox: {
74
+ noResults: "No results",
75
+ clearSelection: "Clear selection",
76
+ },
77
+ emptyState: {
78
+ illustrationAlt: "No data illustration",
79
+ },
80
+ tag: {
81
+ remove: "Remove",
82
+ },
83
+ modal: {
84
+ closeLabel: "Close",
85
+ },
86
+ drawer: {
87
+ closeLabel: "Close",
88
+ },
89
+ toast: {
90
+ closeNotification: "Close notification",
91
+ },
92
+ confirmDialog: {
93
+ cancel: "Cancel",
94
+ },
95
+ menu: {
96
+ submenuAriaLabel: "{{text}} submenu",
97
+ },
98
+ formField: {
99
+ optional: "Optional",
100
+ },
101
+ segment: {
102
+ segmentControl: "Segment control",
103
+ },
104
+ stepper: {
105
+ progress: "Progress",
106
+ },
107
+ },
108
+ dataEditor: {
109
+ modal: {
110
+ importTitle: "Import Data",
111
+ editTitle: "Edit Data",
112
+ },
113
+ confirmClose: {
114
+ title: "Discard changes?",
115
+ text: "You have unsaved changes. Are you sure you want to close?",
116
+ action: "Discard",
117
+ },
118
+ confirmSubmit: {
119
+ title: "Confirm submission",
120
+ text: "You are about to submit the following changes:",
121
+ createRows_one: "{{count}} new row will be created",
122
+ createRows_other: "{{count}} new rows will be created",
123
+ updateRows_one: "{{count}} row will be updated",
124
+ updateRows_other: "{{count}} rows will be updated",
125
+ deleteRows_one: "{{count}} row will be deleted",
126
+ deleteRows_other: "{{count}} rows will be deleted",
127
+ bannerTitle_one: "{{count}} row has errors",
128
+ bannerTitle_other: "{{count}} rows have errors",
129
+ action: "Submit",
130
+ },
131
+ footer: {
132
+ submit: "Submit",
133
+ import: "Import",
134
+ next: "Next",
135
+ cancel: "Cancel",
136
+ back: "Back",
137
+ export: "Export",
138
+ exportAll: "Export all",
139
+ exportFiltered: "Export filtered",
140
+ csvFormat: "Comma-separated values (.csv)",
141
+ tsvFormat: "Tab-separated values (.tsv)",
142
+ excelFormat: "Microsoft Excel (.xlsx)",
143
+ jsonFormat: "JSON (.json)",
144
+ xmlFormat: "XML (.xml)",
145
+ },
146
+ statusBar: {
147
+ totalRows: "{{total}} rows",
148
+ filteredRows: "{{filtered}} of {{total}} rows",
149
+ },
150
+ toast: {
151
+ dismiss: "Dismiss",
152
+ rowMayNotBeVisible:
153
+ "Row has been added but may not be visible due to active filters or sorting",
154
+ },
155
+ grid: {
156
+ emptyTitle: "No data",
157
+ emptyText: "Import data or add rows manually",
158
+ loading: "Loading data...",
159
+ emptyCell: "Empty",
160
+ a11y: {
161
+ gridLabel: "Data editor",
162
+ cellFocus: "Row {{row}}, {{column}}: {{value}}",
163
+ cellFocusEmpty: "Row {{row}}, {{column}}: empty",
164
+ selectAll: "All cells selected",
165
+ rowsSelected: "{{count}} rows selected",
166
+ columnsSelected: "{{count}} columns selected",
167
+ },
168
+ contextMenu: {
169
+ cut: "Cut",
170
+ cutCells: "Cut ({{count}})",
171
+ copy: "Copy",
172
+ copyCells: "Copy ({{count}})",
173
+ paste: "Paste",
174
+ deleteRow: "Delete row",
175
+ deleteRows: "Delete rows ({{count}})",
176
+ restoreRow: "Restore row",
177
+ restoreRows: "Restore rows ({{count}})",
178
+ sortAsc: "Sort A to Z",
179
+ sortDesc: "Sort Z to A",
180
+ sortRemove: "Remove sorting",
181
+ insertAbove: "Insert row above",
182
+ insertBelow: "Insert row below",
183
+ duplicateRow: "Duplicate row",
184
+ cutRow: "Cut row",
185
+ cutRows: "Cut rows ({{count}})",
186
+ copyRow: "Copy row",
187
+ copyRows: "Copy rows ({{count}})",
188
+ clearRow: "Clear row",
189
+ clearRows: "Clear rows ({{count}})",
190
+ clearAll: "Clear all",
191
+ pin: "Pin column",
192
+ pinColumns: "Pin columns",
193
+ unpin: "Unpin column",
194
+ unpinColumns: "Unpin columns",
195
+ cutColumn: "Cut column",
196
+ copyColumn: "Copy column",
197
+ pasteColumn: "Paste column",
198
+ lockColumn: "Lock column",
199
+ lockColumns: "Lock columns",
200
+ lockedCannotClear: "Can't clear a locked column",
201
+ lockedCannotDelete: "Can't delete a locked column",
202
+ lockedCannotRevert: "Can't revert a locked column",
203
+ lockedCannotUnlock: "This column was locked by configuration",
204
+ unlockColumn: "Unlock column",
205
+ unlockColumns: "Unlock columns",
206
+ clearColumn: "Clear column",
207
+ clearColumns: "Clear columns",
208
+ deleteColumn: "Delete column",
209
+ deleteColumns: "Delete columns",
210
+ editColumn: "Edit column",
211
+ transform: "Transform",
212
+ transformMerge: "Merge",
213
+ transformSplit: "Split",
214
+ formula: "Formula",
215
+ lockedCannotFormula: "Can't apply a formula to a locked column",
216
+ revertChanges: "Revert changes",
217
+ },
218
+ formulaModal: {
219
+ title: "Apply formula",
220
+ apply: "Apply",
221
+ applyToRows_one: "Apply to {{count}} row",
222
+ applyToRows_other: "Apply to {{count}} rows",
223
+ inputLabel: "Formula",
224
+ placeholder: "=UPPER(value) · Type @ to browse",
225
+ groupFunctions: "Functions",
226
+ groupColumns: "Columns",
227
+ groupKeywords: "Keywords",
228
+ keywordValue: "Current cell value",
229
+ preview: "Preview",
230
+ errors: {
231
+ "unexpected-character": "'{{char}}' isn't valid here",
232
+ "unexpected-escape":
233
+ "Invalid escape '\\{{char}}' — only \\} and \\\\ are allowed inside @{}",
234
+ "unterminated-column-ref": "@{...} isn't closed — add a closing }",
235
+ "expected-equals": "Formulas must start with =",
236
+ "expected-expression": "Add a function, value, or @column after =",
237
+ "expected-open-paren": "Missing '(' after {{name}}",
238
+ "expected-close-paren": "Missing closing ')'",
239
+ "expected-column-name":
240
+ "Add a column name after @ — e.g. @Name or @{First Name}",
241
+ "unexpected-token": "'{{token}}' isn't expected here",
242
+ "unknown-function": "No function named '{{name}}'",
243
+ "unknown-column": "No column named '{{name}}'",
244
+ "arity-mismatch-too-few":
245
+ "{{name}} expects at least {{min}} arguments — got {{actual}}",
246
+ "arity-mismatch-too-many":
247
+ "{{name}} takes at most {{max}} arguments — got {{actual}}",
248
+ "function-not-in-expression-mode":
249
+ "'{{name}}' can't be used here — use Merge or Split from the Transform menu",
250
+ "column-not-numeric":
251
+ "'{{name}}' is not a number column — arithmetic requires number columns",
252
+ },
253
+ },
254
+ formulas: {
255
+ UPPER: { label: "Uppercase" },
256
+ LOWER: { label: "Lowercase" },
257
+ TRIM: { label: "Trim whitespace" },
258
+ CAPITALIZE: { label: "Capitalize" },
259
+ ROUND: { label: "Round" },
260
+ ROUND_UP: { label: "Round up" },
261
+ ROUND_DOWN: { label: "Round down" },
262
+ ABS: { label: "Absolute value" },
263
+ CLEAR: { label: "Clear" },
264
+ CONCAT: { label: "Concatenate" },
265
+ LEFT: { label: "Left" },
266
+ RIGHT: { label: "Right" },
267
+ MID: { label: "Mid" },
268
+ SUBSTITUTE: { label: "Substitute" },
269
+ MERGE: { label: "Merge columns" },
270
+ SPLIT: { label: "Split column" },
271
+ },
272
+ transformMerge: {
273
+ addColumn: "Add column",
274
+ apply: "Apply",
275
+ deleteSource: "Delete source columns",
276
+ deleteSourceDescription:
277
+ "Columns added during import can be deleted. Schema columns will remain.",
278
+ from: "From",
279
+ into: "Into",
280
+ joinWith: "Join with",
281
+ joinWithPlaceholder: "e.g. space, comma",
282
+ preview: "Preview",
283
+ removeColumn: "Remove column",
284
+ applyToRows_one: "Apply to {{count}} row",
285
+ applyToRows_other: "Apply to {{count}} rows",
286
+ selectColumn: "Select column",
287
+ title: "Merge",
288
+ },
289
+ transformSplit: {
290
+ addColumn: "Add column",
291
+ apply: "Apply",
292
+ applyToRows_one: "Apply to {{count}} row",
293
+ applyToRows_other: "Apply to {{count}} rows",
294
+ deleteSource: "Delete source column",
295
+ deleteSourceDescription:
296
+ "Columns added during import can be deleted. Schema columns will remain.",
297
+ from: "From",
298
+ into: "Into",
299
+ preview: "Preview",
300
+ removeColumn: "Remove column",
301
+ selectColumn: "Select column",
302
+ splitBy: "Split by",
303
+ splitByPlaceholder: "e.g. space, comma",
304
+ title: "Split",
305
+ },
306
+ phase: {
307
+ splitting: "Splitting columns...",
308
+ merging: "Merging columns...",
309
+ pasting: "Pasting data...",
310
+ clearing: "Clearing cells...",
311
+ filling: "Filling cells...",
312
+ sorting: "Sorting rows...",
313
+ undoing: "Reverting changes...",
314
+ redoing: "Reapplying changes...",
315
+ deleting: "Deleting rows...",
316
+ deletingSource: "Removing data source...",
317
+ importing: "Importing data...",
318
+ transforming: "Transforming...",
319
+ default: "Processing...",
320
+ },
321
+ },
322
+ dataSources: {
323
+ title: "Data Sources",
324
+ importData: "Import data",
325
+ addRow: "Add row",
326
+ manuallyAdded: "Manually added",
327
+ existingData: "Existing data",
328
+ addDataSource: "Add data source",
329
+ show: "Show",
330
+ hide: "Hide",
331
+ remove: "Remove",
332
+ confirmRemove: {
333
+ title: "Remove data source?",
334
+ text: "All data imported from this source will be removed.",
335
+ action: "Remove",
336
+ },
337
+ },
338
+ filters: {
339
+ title: "Filters",
340
+ search: "Search",
341
+ replace: "Replace",
342
+ findAndReplace: "Find & Replace",
343
+ searchMode: "Search mode",
344
+ matchCase: "Match case",
345
+ matchEntireCell: "Match entire cell",
346
+ rows: "Rows",
347
+ columns: "Columns",
348
+ searchInColumns: "Search in columns",
349
+ allColumns: "All columns",
350
+ rowsWithErrors: "Rows with errors",
351
+ editedRows: "Changed rows",
352
+ newRows: "New rows",
353
+ rowsWithEmptyCells: "Rows with empty cells",
354
+ deletedRows: "Deleted rows",
355
+ previousOccurrence: "Previous occurrence",
356
+ nextOccurrence: "Next occurrence",
357
+ zeroResults: "No results found",
358
+ allValues: "All values",
359
+ min: "Min",
360
+ max: "Max",
361
+ from: "From",
362
+ to: "To",
363
+ replaceAll: "Replace all",
364
+ resetFilters: "Reset filters",
365
+ showMore: "View error details",
366
+ showLess: "Hide error details",
367
+ },
368
+ undoRedo: {
369
+ undo: "Undo",
370
+ redo: "Redo",
371
+ },
372
+ validation: {
373
+ required: "This field is required",
374
+ invalidNumber: "Invalid number",
375
+ invalidEmail: "Invalid email address",
376
+ invalidDate: "Invalid date",
377
+ endDateBeforeStart: "End date must be after start date",
378
+ },
379
+ license: {
380
+ loading: "Loading data editor",
381
+ errors: {
382
+ LICENSE_INVALID: {
383
+ title: "Invalid license",
384
+ text: "Please contact support for assistance.",
385
+ },
386
+ MISSING_API_KEY: {
387
+ title: "Missing API key",
388
+ text: "No API key was provided. Please configure a valid API key.",
389
+ },
390
+ DOMAIN_NOT_ALLOWED: {
391
+ title: "Domain not allowed",
392
+ text: "This domain is not authorized for this license. Please check your domain settings.",
393
+ },
394
+ SUBSCRIPTION_INACTIVE: {
395
+ title: "Subscription inactive",
396
+ text: "Your subscription is no longer active. Please renew to continue.",
397
+ },
398
+ },
399
+ },
400
+ errorState: {
401
+ title: "Something went wrong",
402
+ text: "Please close this window and try again",
403
+ },
404
+ uploader: {
405
+ steps: {
406
+ selectFiles: "Select files",
407
+ sheetSelection: "Select sheet",
408
+ matchColumns: "Match columns",
409
+ matchValues: "Match values",
410
+ primaryKey: "Primary key",
411
+ },
412
+ uploadFile: {
413
+ title: "Upload file",
414
+ text: "Upload a CSV, TSV, Excel, JSON or XML file to import data",
415
+ textDynamic: "Upload a {{formats}} file to import data",
416
+ parseError: "Failed to parse file",
417
+ clickToUpload: "Click to upload",
418
+ orDragAndDrop: "or drag and drop",
419
+ formats: "CSV, TSV, XLSX, JSON, XML",
420
+ downloadExample: "Download sample file",
421
+ },
422
+ remoteSources: {
423
+ browseFile: "Browse file",
424
+ emptyData: "The source returned no data",
425
+ fetchError: "Failed to load data",
426
+ },
427
+ matchColumns: {
428
+ title: "Match columns",
429
+ text: "Map imported columns to existing columns",
430
+ banner: "Some columns could not be automatically matched",
431
+ importedColumns: "Imported columns",
432
+ targetColumns: "Target columns",
433
+ unmatchedCount: "{{count}} unmatched",
434
+ selectColumnPlaceholder: "Select column",
435
+ bestMatch: "Best match",
436
+ createColumn: "Create column",
437
+ createColumnNameLabel: "Column name",
438
+ createColumnNamePlaceholder: "Enter column name",
439
+ createColumnNameTaken: "A column with this name already exists",
440
+ createColumnSubmit: "Create",
441
+ createColumnTitle: "Create column",
442
+ editColumnSubmit: "Save",
443
+ editColumnTitle: "Edit column",
444
+ createColumnWarning: "A matching column already exists",
445
+ newColumnTag: "New",
446
+ unmatchedWarning:
447
+ "Unmatched columns won't be imported. Match this column to keep the data. You can transform columns after importing.",
448
+ },
449
+ sheetSelection: {
450
+ title: "Select sheet",
451
+ text: "This file contains multiple sheets. Choose which sheet to import.",
452
+ rowCount: "{{count}} rows",
453
+ emptySheet: "Empty sheet",
454
+ },
455
+ matchValues: {
456
+ title: "Match values",
457
+ text: "Review how imported values map to your column options. Adjust any mappings that need a different match.",
458
+ importedValues: "Imported values",
459
+ targetValue: "Target value",
460
+ fullyMatched: "All matched",
461
+ unmatchedCount: "{{matched}}/{{total}} matched",
462
+ selectValuePlaceholder: "Select value",
463
+ showMatched: "Show matched",
464
+ allMatched: "All values are matched",
465
+ unmatchedWarning: "Unmatched values won't be imported",
466
+ },
467
+ primaryKey: {
468
+ title: "Select primary key",
469
+ text: "Choose the column that uniquely identifies each row",
470
+ none: "No primary key",
471
+ noneHint: "All rows will be appended as new entries without deduplication.",
472
+ },
473
+ },
474
+ chat: {
475
+ emptyTitle: "What would you like to fix?",
476
+ header: "Chat with AI",
477
+ placeholder: "Ask AI...",
478
+ send: "Send",
479
+ },
480
+ common: {
481
+ cancel: "Cancel",
482
+ collapse: "Collapse",
483
+ expand: "Expand",
484
+ remove: "Remove",
485
+ search: "Search",
486
+ },
487
+ },
488
+ };
489
+
490
+ type PluralSuffixes = "_zero" | "_one" | "_two" | "_few" | "_many" | "_other";
491
+
492
+ /** Makes all properties optional recursively. */
493
+ type DeepPartial<T> = {
494
+ [P in keyof T]?: T[P] extends Record<string, unknown>
495
+ ? DeepPartial<T[P]>
496
+ : T[P];
497
+ };
498
+
499
+ /**
500
+ * Expands any key ending in a plural suffix to include all 6 CLDR plural suffixes.
501
+ * e.g. `{ createRows_one: string; createRows_other: string }` becomes
502
+ * `{ createRows_zero?: string; createRows_one?: string; createRows_two?: string; ... }`
503
+ */
504
+ type ExpandPluralKeys<T> = T extends Record<string, unknown>
505
+ ? {
506
+ [K in keyof T as K extends `${infer Base}${PluralSuffixes}`
507
+ ? `${Base}_zero` | `${Base}_one` | `${Base}_two` | `${Base}_few` | `${Base}_many` | `${Base}_other`
508
+ : K]: T[K] extends Record<string, unknown> ? ExpandPluralKeys<T[K]> : T[K];
509
+ }
510
+ : T;
511
+
512
+ /**
513
+ * Consumer-facing translations type. All keys optional — consumers override only what they need.
514
+ * Plural keys are expanded to all 6 CLDR forms.
515
+ */
516
+ type DataEditorTranslations = DeepPartial<ExpandPluralKeys<typeof export_default>>;
517
+
518
+ type UpdogErrorCode = "PARSE_ERROR" | "RENDER_ERROR" | "TRANSFORM_ERROR" | "VALIDATION_ERROR" | "WORKER_ERROR" | "COMMAND_ERROR" | "OPERATION_ERROR";
519
+ type UpdogError = {
520
+ code: UpdogErrorCode;
521
+ message: string;
522
+ source: string;
523
+ originalError?: unknown;
524
+ };
525
+
526
+ /**
527
+ * Base row shape. Each key is a column ID, each value is the cell data.
528
+ * Extend this with your own type for type-safe column access.
529
+ *
530
+ * @example
531
+ * ```ts
532
+ * type Employee = { id: string; name: string; email: string };
533
+ * <DataEditor<Employee> columns={...} primaryKey="id" />
534
+ * ```
535
+ */
536
+ type DataEditorRow = Record<string, unknown>;
537
+ type SortDirection = "asc" | "desc";
538
+ type SortState = {
539
+ columnId: string;
540
+ direction: SortDirection;
541
+ } | null;
542
+ type Filters = {
543
+ search: string;
544
+ matchCase: boolean;
545
+ matchEntireCell: boolean;
546
+ errorMessageFilters: string[];
547
+ showOnlyNewRows: boolean;
548
+ showOnlyEditedRows: boolean;
549
+ showOnlyEmptyCells: boolean;
550
+ /** When true, show only rows flagged for deletion (bin mode). All other filters are bypassed. */
551
+ showOnlyDeletedRows: boolean;
552
+ filterColumns: string[] | null;
553
+ /** Per-column value filters. Key = column ID, value = allowed display-formatted strings. */
554
+ columnValueFilters: Record<string, string[]>;
555
+ /** Per-column numeric range filters. Key = column ID. */
556
+ columnRangeFilters: Record<string, {
557
+ min?: number;
558
+ max?: number;
559
+ }>;
560
+ /** Per-column date range filters. Key = column ID, values are ISO date strings (YYYY-MM-DD). */
561
+ columnDateRangeFilters: Record<string, {
562
+ min?: string;
563
+ max?: string;
564
+ }>;
565
+ };
566
+
567
+ type ChatResponseChunk<TRow extends DataEditorRow = DataEditorRow> = {
568
+ type: "status";
569
+ content: string;
570
+ } | {
571
+ type: "message";
572
+ content: string;
573
+ } | {
574
+ type: "rows";
575
+ content: TRow[];
576
+ } | {
577
+ type: "transform";
578
+ content: string;
579
+ };
580
+ type ChatRowStatus = "new" | "edited" | "original";
581
+ type ChatRow<TRow extends DataEditorRow = DataEditorRow> = {
582
+ data: TRow;
583
+ status: ChatRowStatus;
584
+ errors: Record<string, string[]>;
585
+ source: string;
586
+ };
587
+ type ChatErrorSummary = {
588
+ field: string;
589
+ message: string;
590
+ count: number;
591
+ examples: string[];
592
+ };
593
+ type ChatDataContext<TRow extends DataEditorRow = DataEditorRow> = {
594
+ columns: DataEditorColumn[];
595
+ primaryKey: keyof TRow;
596
+ totalRowCount: number;
597
+ filteredRowCount: number;
598
+ sample: ChatRow<TRow>[];
599
+ errorSummary: ChatErrorSummary[];
600
+ getRows: () => ChatRow<TRow>[];
601
+ };
602
+ type ChatContext<TRow extends DataEditorRow = DataEditorRow> = ChatDataContext<TRow> & {
603
+ message: string;
604
+ };
605
+ type DataEditorChat<TRow extends DataEditorRow = DataEditorRow> = {
606
+ sampleSize?: number;
607
+ emptyTitle?: string;
608
+ suggestionsCount?: number;
609
+ loadSuggestions?: (context: ChatDataContext<TRow>) => Promise<string[]>;
610
+ onMessage: (context: ChatContext<TRow>) => AsyncIterable<ChatResponseChunk<TRow>>;
611
+ onCancel?: () => void;
612
+ };
613
+
614
+ /** Severity level for a validation message. */
615
+ type ValidationLevel = "error";
616
+ /**
617
+ * A single validation message attached to a cell.
618
+ * Return this from a `CellValidator` to flag a problem.
619
+ */
620
+ type ValidationError = {
621
+ level: ValidationLevel;
622
+ /** Human-readable message shown in the cell tooltip. */
623
+ message: string;
624
+ };
625
+ /**
626
+ * A function that validates a single cell value.
627
+ * Return a `ValidationError` to flag a problem, or `null` if the value is valid.
628
+ *
629
+ * Built-in validator factories: `required(msg)`, `numeric(msg)`, `email(msg)`,
630
+ * `date(msg)`, `endDateAfterStart(startField, msg)`. Import them from the package root.
631
+ *
632
+ * @param value - The current cell value.
633
+ * @param row - The full row, useful for cross-field checks.
634
+ */
635
+ type CellValidator = (value: unknown, row: DataEditorRow) => ValidationError | null;
636
+ /** Text input cell. This is the default editor when no `editor` is specified. */
637
+ type TextEditorCell = {
638
+ type: "text";
639
+ };
640
+ /** Date picker cell. Optionally restrict the selectable date range. */
641
+ type DateEditorCell = {
642
+ type: "date";
643
+ /** Earliest selectable date. */
644
+ minDate?: Date;
645
+ /** Latest selectable date. */
646
+ maxDate?: Date;
647
+ };
648
+ /** Dropdown select cell. The user picks from a fixed list of options. */
649
+ type SelectEditorCell = {
650
+ type: "select";
651
+ /** The list of options shown in the dropdown. Each string is both the stored value and the display label. */
652
+ options: string[];
653
+ };
654
+ /** Number input cell with locale-aware formatting. */
655
+ type NumberEditorCell = {
656
+ type: "number";
657
+ /** Maximum number of decimal digits allowed. When omitted, decimals are unrestricted. */
658
+ decimalPlaces?: number;
659
+ /** Character used as the decimal point (e.g. `"."` or `","`). Defaults to the browser locale. */
660
+ decimalSeparator?: string;
661
+ /** Character inserted between groups of three digits (e.g. `","` or `"."`). Defaults to the browser locale. */
662
+ thousandsSeparator?: string;
663
+ /** Extra characters to allow beyond digits, decimal separator, and minus sign (e.g. `"%-"`). When defined, minus is only kept if explicitly included. */
664
+ allowChars?: string;
665
+ };
666
+ /**
667
+ * Controls how a cell is edited.
668
+ *
669
+ * - `"text"` — plain text input (default).
670
+ * - `"date"` — date picker with optional min/max bounds.
671
+ * - `"select"` — dropdown with a fixed list of options.
672
+ * - `"number"` — number input with locale-aware formatting.
673
+ */
674
+ type CellEditor = TextEditorCell | DateEditorCell | SelectEditorCell | NumberEditorCell;
675
+ /** Dropdown filter shown in the sidebar Filters panel. */
676
+ type SelectColumnFilter = {
677
+ type: "select";
678
+ /** Label displayed above the filter. */
679
+ label?: string;
680
+ /** Placeholder text when nothing is selected. */
681
+ placeholder?: string;
682
+ /** Fixed list of filter options. When omitted, options are derived from column values. */
683
+ options?: string[];
684
+ /** Allow selecting multiple values at once. */
685
+ multiple?: boolean;
686
+ };
687
+ /** Numeric min/max range filter shown in the sidebar Filters panel. */
688
+ type NumberRangeColumnFilter = {
689
+ type: "number-range";
690
+ /** Label displayed above the filter. */
691
+ label?: string;
692
+ };
693
+ /** Date min/max range filter shown in the sidebar Filters panel. */
694
+ type DateRangeColumnFilter = {
695
+ type: "date-range";
696
+ /** Label displayed above the filter. */
697
+ label?: string;
698
+ };
699
+ /**
700
+ * Filter control shown in the sidebar Filters panel for a column.
701
+ *
702
+ * - `"select"` — dropdown to pick one or more values.
703
+ * - `"number-range"` — two inputs for min and max number.
704
+ * - `"date-range"` — two date pickers for start and end date.
705
+ */
706
+ type ColumnFilter = SelectColumnFilter | NumberRangeColumnFilter | DateRangeColumnFilter;
707
+ /** Lock mode for a column. `"all"` locks for every row; `"default"` locks only default-source rows. */
708
+ type ColumnLockMode = "all" | "default";
709
+ /**
710
+ * Defines a column in the editor grid.
711
+ *
712
+ * @example
713
+ * ```ts
714
+ * const columns: DataEditorColumn[] = [
715
+ * { id: "name", title: "Full Name", width: 200, validate: required },
716
+ * { id: "email", title: "Email", width: 250, validate: [required, email], unique: true },
717
+ * { id: "role", title: "Role", editor: { type: "select", options: roleOptions } },
718
+ * { id: "salary", title: "Salary", validate: numeric, formatter: (v) => `$${v}` },
719
+ * ];
720
+ * ```
721
+ */
722
+ type DataEditorColumn = {
723
+ /** Unique column identifier. Must match the keys in your row data. */
724
+ id: string;
725
+ /** Column header text. */
726
+ title: string;
727
+ /** One or more validators to run on every edit. */
728
+ validate?: CellValidator | CellValidator[];
729
+ /** When `true`, the editor flags duplicate values in this column as errors. */
730
+ unique?: boolean;
731
+ /** Column IDs to revalidate when this column changes. Use for cross-field rules like "end date must be after start date". */
732
+ dependentFields?: string[];
733
+ /** Format the display value without changing stored data. E.g. add `$` prefix. */
734
+ formatter?: (value: string) => string;
735
+ /** Transform the value before it enters the store. Runs on every edit and import. */
736
+ transformer?: (value: unknown) => unknown;
737
+ /** How the cell is edited. Defaults to text input. */
738
+ editor?: CellEditor;
739
+ /** Adds a filter control for this column in the sidebar Filters panel. */
740
+ filter?: ColumnFilter;
741
+ /** Whether this column can be pinned to the left via the header context menu. @default true */
742
+ pinnable?: boolean;
743
+ /** Column width in pixels. @default 150 */
744
+ size?: number;
745
+ /**
746
+ * Controls whether cells in this column are locked.
747
+ * - `true` | `"all"` — locked for all rows.
748
+ * - `"default"` — locked only for default-source rows; non-default-source rows remain editable.
749
+ * - `false` | `undefined` — not locked.
750
+ */
751
+ locked?: boolean | ColumnLockMode;
752
+ };
753
+
754
+ /** Params passed to `findAndReplace.onFind` when the user types a search query. */
755
+ type FindParams = {
756
+ /** The search string. */
757
+ search: string;
758
+ /** When `true`, matching is case-sensitive. */
759
+ matchCase?: boolean;
760
+ /** When `true`, the entire cell value must equal the search string. */
761
+ matchEntireCell?: boolean;
762
+ /** Restrict search to these columns. `null` or omitted = all columns. */
763
+ columnIds?: string[] | null;
764
+ /** Current view filters so the server can scope matches to the active filter set. */
765
+ filters?: QueryFilters;
766
+ /** Current sort state so match ordering follows the visual row order. */
767
+ sort?: SortState;
768
+ };
769
+ /** A single match location returned by the server. */
770
+ type FindMatch = {
771
+ /** Row position in the current filtered+sorted view (for grid scrolling). */
772
+ rowIndex: number;
773
+ /** Column ID where the match occurs. */
774
+ columnId: string;
775
+ /** Character offset within the cell value (for inline highlight). */
776
+ startIndex: number;
777
+ /** 0-based position in the ordered match list (for counter display). */
778
+ matchIndex: number;
779
+ };
780
+ /** Response from `findAndReplace.onFind`. */
781
+ type FindResponse = {
782
+ /** Total number of matches across all rows. */
783
+ totalCount: number;
784
+ /** The first match. Omit when `totalCount` is 0. */
785
+ current?: FindMatch;
786
+ };
787
+ /** Params passed to `findAndReplace.onNavigate` when the user clicks prev/next. */
788
+ type FindNavigateParams = FindParams & {
789
+ /** Navigation direction. */
790
+ direction: "next" | "prev";
791
+ /** Current match position so the server knows where to navigate from. */
792
+ currentMatchIndex: number;
793
+ };
794
+ /** Params passed to `findAndReplace.onReplace`. */
795
+ type ReplaceParams = FindParams & {
796
+ /** The replacement text. */
797
+ replacement: string;
798
+ /** When `true`, replace all matches. When omitted or `false`, replace only `target`. */
799
+ all?: boolean;
800
+ /** The specific match to replace. Required when `all` is not `true`. */
801
+ target?: FindMatch;
802
+ };
803
+ /** Response from `findAndReplace.onReplace`. */
804
+ type ReplaceResponse = {
805
+ /** Remaining match count after replacement. */
806
+ totalCount: number;
807
+ /** Next match to navigate to after replacement. Omit when none left. */
808
+ current?: FindMatch;
809
+ };
810
+ /** Server-side find and replace configuration. */
811
+ type FindAndReplaceConfig = {
812
+ /** Called when the user types a search query. Returns total count and first match. */
813
+ onFind: (params: FindParams) => Promise<FindResponse>;
814
+ /** Called when the user clicks prev/next arrows. Returns the target match. */
815
+ onNavigate: (params: FindNavigateParams) => Promise<FindMatch>;
816
+ /** Called when the user clicks Replace or Replace All. */
817
+ onReplace: (params: ReplaceParams) => Promise<ReplaceResponse>;
818
+ };
819
+ type ServerRowId = string | number;
820
+ /** Row-level status flags returned by the server. Drive row filters and sidebar counts. */
821
+ type ServerRowStatus = {
822
+ edited?: boolean;
823
+ new?: boolean;
824
+ deleted?: boolean;
825
+ hasErrors?: boolean;
826
+ hasEmptyCells?: boolean;
827
+ };
828
+ /** Server-reported change for a single cell. The current value lives in `fields`. */
829
+ type ServerCellChange = {
830
+ original: unknown;
831
+ };
832
+ /** Server-reported validation error for a single cell. */
833
+ type ServerCellError = {
834
+ message: string;
835
+ code?: number | string;
836
+ };
837
+ /** Per-row metadata returned by the server in server-delegated mode. */
838
+ type ServerRowMeta = {
839
+ /** Row-level status flags — drive row filters and sidebar counts. */
840
+ status?: ServerRowStatus;
841
+ /** Cell-level change tracking. Key = field name. */
842
+ changes?: Record<string, ServerCellChange>;
843
+ /** Cell-level validation errors. Key = field name. */
844
+ errors?: Record<string, ServerCellError[]>;
845
+ };
846
+ /** Per-source row count returned by the server. Drives the Data Sources sidebar. */
847
+ type ServerSourceCount = {
848
+ id: string;
849
+ name: string;
850
+ count: number;
851
+ };
852
+ /** Aggregate row counts returned alongside a query page. Drive sidebar indicators. */
853
+ type ServerQueryCounts = {
854
+ edited?: number;
855
+ new?: number;
856
+ deleted?: number;
857
+ errors?: number;
858
+ emptyCells?: number;
859
+ sources?: ServerSourceCount[];
860
+ };
861
+ type ServerRow<T extends DataEditorRow = DataEditorRow> = {
862
+ id: ServerRowId;
863
+ fields: T;
864
+ meta?: ServerRowMeta;
865
+ };
866
+ type ServerResponse<T> = {
867
+ data: T;
868
+ meta?: Record<string, unknown>;
869
+ };
870
+ type QueryFilters<F = Record<string, unknown>> = Partial<Filters> & F;
871
+ type QueryParams<F = Record<string, unknown>> = {
872
+ filters?: QueryFilters<F>;
873
+ sort?: SortState;
874
+ /** When present, only rows belonging to these source IDs are returned. Omit to include all sources. */
875
+ sources?: string[];
876
+ offset?: number;
877
+ limit: number;
878
+ signal?: AbortSignal;
879
+ };
880
+ type QueryResponse<T extends DataEditorRow = DataEditorRow> = ServerResponse<{
881
+ rows: ServerRow<T>[];
882
+ totalCount: number;
883
+ filteredCount?: number;
884
+ counts?: ServerQueryCounts;
885
+ }>;
886
+ /**
887
+ * Filter options returned by `onFilterOptions` for populating sidebar filter controls in server mode.
888
+ * Keys are column IDs. Only include columns that have a `filter` configured.
889
+ */
890
+ type FilterOptionsResponse = {
891
+ [columnId: string]: {
892
+ /** Values for `"select"` filters. Raw display strings — no formatter is applied. */
893
+ options?: string[];
894
+ /** Bounds for `"number-range"` filters. */
895
+ range?: {
896
+ min: number;
897
+ max: number;
898
+ };
899
+ /** Bounds for `"date-range"` filters. Values are ISO date strings (YYYY-MM-DD). */
900
+ dateRange?: {
901
+ min: string;
902
+ max: string;
903
+ };
904
+ };
905
+ };
906
+ type ServerCallOptions = {
907
+ signal: AbortSignal;
908
+ };
909
+ /**
910
+ * Coordinate rectangle within the server's data view.
911
+ * Uses ServerRowId (primary key) for rows and column ID strings for columns.
912
+ *
913
+ * Convention:
914
+ * - Both row fields omitted → all rows.
915
+ * - Both column fields omitted → all columns.
916
+ * - Single row: `fromRow` AND `toRow` both set, `toRow === fromRow`.
917
+ * - Single column: `fromColumn` AND `toColumn` both set, `toColumn === fromColumn`.
918
+ * - Range: both `from` and `to` set, `to !== from`.
919
+ * - `allSelected: true` → all rows and all columns.
920
+ * - Empty `{}` → used only for insert operations.
921
+ *
922
+ * INVALID: `from` present without `to`, or vice versa.
923
+ */
924
+ type Region = {
925
+ fromRow?: ServerRowId;
926
+ toRow?: ServerRowId;
927
+ fromColumn?: string;
928
+ toColumn?: string;
929
+ allSelected?: boolean;
930
+ };
931
+ /** Describes where and how to insert a new row. */
932
+ type InsertParams = {
933
+ /** Existing row to anchor the insert relative to. Omitted when appending to the end. */
934
+ anchorRow?: ServerRowId;
935
+ /** Insert before or after the anchor row. */
936
+ position: "above" | "below";
937
+ /** Column IDs matching the order of `values` entries. */
938
+ columns: string[];
939
+ };
940
+ /**
941
+ * Unified edit params for all data mutations in server-delegated mode.
942
+ *
943
+ * The combination of fields determines the operation:
944
+ * - `target` + `values` → cell edit, clear, or external paste
945
+ * - `target` + `source` → internal paste (+ `cut` for cut-paste)
946
+ * - `target` + `source` (fill) → fill handle
947
+ * - `target` + `transform` → transform / revert
948
+ * - `target` + `delete: true` → mark rows for deletion
949
+ * - `target` + `delete: false` → restore rows (unmark deletion)
950
+ * - `insert` + `values` → create a new row
951
+ *
952
+ * `values` is always a 2D array. For a single cell edit: `[["newValue"]]`.
953
+ * For clearing: `[[""]]`. The server interprets dimensions relative to `target`:
954
+ * a 1×1 `values` applied to a multi-cell target means "fill all cells with this value".
955
+ *
956
+ * `filters` and `sort` provide the view context so the server can resolve
957
+ * which rows fall between `fromRow` and `toRow` in the current view.
958
+ * Omitted for single-cell edits where `fromRow` is a direct row ID.
959
+ */
960
+ type EditParams = {
961
+ target: Region[];
962
+ source?: Region[];
963
+ values?: unknown[][];
964
+ transform?: TransformParams;
965
+ cut?: boolean;
966
+ delete?: boolean;
967
+ insert?: InsertParams;
968
+ undo?: boolean;
969
+ filters?: QueryFilters;
970
+ sort?: SortState;
971
+ lockedColumns?: Array<{
972
+ columnId: string;
973
+ mode: "all" | "default";
974
+ }>;
975
+ };
976
+ type ExportParams<F = Record<string, unknown>> = {
977
+ format: "csv" | "tsv" | "xlsx" | "json" | "xml";
978
+ allRows: boolean;
979
+ rtl?: boolean;
980
+ filters?: QueryFilters<F>;
981
+ sort?: SortState;
982
+ signal?: AbortSignal;
983
+ };
984
+ /** Transform operation descriptor. `type` identifies the operation, optional fields carry parameters. */
985
+ type TransformParams = {
986
+ type: string;
987
+ separator?: string;
988
+ /** When `true`, the server should delete the dynamic source columns after applying the transform. */
989
+ deleteSource?: boolean;
990
+ };
991
+ /** Params passed to `onColumnDelete` when the user deletes a dynamic column. */
992
+ type ColumnDeleteParams = {
993
+ /** ID of the dynamic column to delete. */
994
+ columnId: string;
995
+ /** `true` when the server should reverse this operation (undo). Omitted on initial call and redo. */
996
+ undo?: boolean;
997
+ };
998
+ /** Params passed to `onColumnEdit` when the user renames a dynamic column. */
999
+ type ColumnEditParams = {
1000
+ /** ID of the dynamic column being edited. */
1001
+ columnId: string;
1002
+ /** New title for the column. */
1003
+ title: string;
1004
+ /** `true` when the server should reverse this operation (undo). Omitted on initial call and redo. */
1005
+ undo?: boolean;
1006
+ };
1007
+ /** Server's response after applying a mutation. */
1008
+ type EditResponse = {
1009
+ counts?: ServerQueryCounts;
1010
+ /** Business-logic rejection. SDK reverts the optimistic update and shows `reason` in a toast. */
1011
+ rejected?: boolean;
1012
+ /** Why the server rejected. Shown to the user as-is. */
1013
+ reason?: string;
1014
+ /** The full row created by the server. Returned for insert operations. */
1015
+ row?: ServerRow;
1016
+ /** Updated column list. When present, the SDK replaces its columns with this list. */
1017
+ columns?: Array<{
1018
+ id: string;
1019
+ title: string;
1020
+ }>;
1021
+ };
1022
+ /**
1023
+ * Every decision the user made during the import wizard, packed into one object.
1024
+ * You get the raw file and the full mapping config. Parse it however you want —
1025
+ * stream it, bulk-load it, hand it to a background job. Your call.
1026
+ */
1027
+ type ImportMappings = {
1028
+ /** CSV header → column ID. Headers the user left unmatched are `undefined`. */
1029
+ columnMapping: Record<string, string | undefined>;
1030
+ /**
1031
+ * Value substitutions for select columns.
1032
+ * Outer key = column ID, inner key = imported value, inner value = target option.
1033
+ * Only present when at least one select column was matched.
1034
+ */
1035
+ valueMapping: Record<string, Record<string, string | undefined>>;
1036
+ /** Column ID used to match imported rows against existing data. Same value as `DataEditorProps.primaryKey`. */
1037
+ primaryKey: string;
1038
+ /** Sheet name the user selected. Only present for multi-sheet XLSX files. */
1039
+ selectedSheet?: string;
1040
+ /** Zero-based index of the row the SDK detected as the header row. */
1041
+ headerRowIndex: number;
1042
+ /** Number format detected from the file contents. Affects how `"1.234,56"` vs `"1,234.56"` is read. */
1043
+ numberFormat: "EU" | "US";
1044
+ /**
1045
+ * Per-column date disambiguation. Key = CSV header (not column ID).
1046
+ * `"EU"` = day-first, `"US"` = month-first.
1047
+ * Only includes columns where the format was ambiguous and had to be resolved.
1048
+ */
1049
+ dateFormats: Record<string, "EU" | "US">;
1050
+ /**
1051
+ * Columns the user created during import for unmatched headers.
1052
+ * These don't exist in your schema yet — you decide whether to persist them.
1053
+ */
1054
+ newColumns: DataEditorColumn[];
1055
+ };
1056
+ /** Params passed to `onFileImport`. The original file, unchanged, plus everything the wizard collected. */
1057
+ type FileImportParams = {
1058
+ /** The original file. Same bytes the user dropped into the browser. */
1059
+ file: File;
1060
+ /** All mapping decisions from the import wizard. */
1061
+ mappings: ImportMappings;
1062
+ /** Display name for this import source. Typically the file name. */
1063
+ sourceName: string;
1064
+ /** Fires when the user cancels the import. */
1065
+ signal?: AbortSignal;
1066
+ };
1067
+ /**
1068
+ * Params passed to `onRowsImport` once per chunk.
1069
+ * The SDK already parsed the file, applied column mappings, normalized dates
1070
+ * and numbers, and resolved value mappings. You get clean, schema-conformant rows.
1071
+ */
1072
+ type RowsImportParams = {
1073
+ /** Stable ID for this import session. Use it for idempotency or correlation. */
1074
+ importId: string;
1075
+ /** Display name for this import source. Typically the file name. */
1076
+ sourceName: string;
1077
+ /** Zero-based chunk index. Together with `importId`, uniquely identifies each chunk. */
1078
+ chunkIndex: number;
1079
+ /** `true` on the last chunk. Safe to finalize, run post-import hooks, or trigger validation. */
1080
+ isLastChunk: boolean;
1081
+ /** Transformed rows, keyed by column ID. Ready to store. */
1082
+ rows: Record<string, unknown>[];
1083
+ /** Columns the user created during import. Only present on the first chunk (`chunkIndex === 0`). */
1084
+ newColumns?: DataEditorColumn[];
1085
+ /** Column ID used to match imported rows against existing data. */
1086
+ primaryKey: string;
1087
+ /** Fires when the user cancels mid-import. Clean up any partial state. */
1088
+ signal?: AbortSignal;
1089
+ };
1090
+ /** Your response after processing a chunk. */
1091
+ type RowsImportResponse = {
1092
+ /** How many rows you accepted from this chunk. Drives progress reporting. */
1093
+ accepted: number;
1094
+ /** Per-row errors. `rowIndex` is relative to the chunk (0-based). Surfaced in the UI after import. */
1095
+ errors?: Array<{
1096
+ rowIndex: number;
1097
+ message: string;
1098
+ }>;
1099
+ };
1100
+ /** Params passed to `onSourceRemove` when the user deletes a data source. */
1101
+ type SourceRemoveParams = {
1102
+ /** The source ID to remove. Matches the `id` from `ServerQueryCounts.sources`. */
1103
+ sourceId: string;
1104
+ /** Fires when the user cancels. */
1105
+ signal?: AbortSignal;
1106
+ };
1107
+ type DataEditorServer<TRow extends DataEditorRow = DataEditorRow, TFilters = Record<string, unknown>> = {
1108
+ onQuery: (params: QueryParams<TFilters>) => Promise<QueryResponse<TRow>>;
1109
+ /** Fetches filter dictionaries for all filterable columns. Called once after license validation. */
1110
+ onFilterOptions?: () => Promise<FilterOptionsResponse>;
1111
+ /** Called when the user triggers an export. The consumer handles file generation and download. */
1112
+ onExport?: (params: ExportParams<TFilters>) => Promise<void>;
1113
+ /** Called after optimistic local apply. Return `{ rejected: true }` for business-logic errors; throw for infra failures. */
1114
+ onEdit: (params: EditParams, options?: ServerCallOptions) => Promise<EditResponse | void>;
1115
+ /**
1116
+ * File import strategy. You get the raw file and the mapping config.
1117
+ * You own parsing, transformation, and storage.
1118
+ * Mutually exclusive with `onRowsImport`.
1119
+ */
1120
+ onFileImport?: (params: FileImportParams) => Promise<void>;
1121
+ /**
1122
+ * Rows import strategy. The SDK parses the file, applies all mappings,
1123
+ * normalizes dates and numbers, and sends you clean rows in chunks.
1124
+ * Called once per chunk. Awaited before sending the next chunk.
1125
+ * Mutually exclusive with `onFileImport`.
1126
+ */
1127
+ onRowsImport?: (params: RowsImportParams) => Promise<RowsImportResponse | void>;
1128
+ /** Number of rows per import chunk when using `onRowsImport`. @default 5000 */
1129
+ importChunkSize?: number;
1130
+ /** Called when the user removes a data source. Delete all rows belonging to this source. */
1131
+ onSourceRemove?: (params: SourceRemoveParams) => Promise<void>;
1132
+ /** Called when the user deletes, undoes, or redoes a dynamic column deletion. */
1133
+ onColumnDelete?: (params: ColumnDeleteParams) => Promise<EditResponse | void>;
1134
+ /** Called when the user edits a dynamic column (e.g. renames it). */
1135
+ onColumnEdit?: (params: ColumnEditParams) => Promise<EditResponse | void>;
1136
+ /**
1137
+ * Server-side find and replace. When provided, enables Find & Replace UI
1138
+ * in server mode. The server owns all searching and replacing; the SDK
1139
+ * handles counts, navigation, and highlighting.
1140
+ */
1141
+ findAndReplace?: FindAndReplaceConfig;
1142
+ /** Number of rows per page. @default 200 */
1143
+ pageSize?: number;
1144
+ /** Controls how eagerly the SDK fetches data while scrolling. 0 = always debounce, 1 = fetch immediately. @default 0.5 */
1145
+ scrollSensitivity?: number;
1146
+ };
1147
+
1148
+ /** Actions available inside the `onComplete` callback. */
1149
+ type DataEditorActions = {
1150
+ /** Discard all changes and reload the original data. */
1151
+ reset: () => void;
1152
+ };
1153
+ /**
1154
+ * A single row in the submit result, with orthogonal flags describing its state.
1155
+ *
1156
+ * - `isNew` — provenance: the row was imported or manually added this session.
1157
+ * Permanent from creation; does not flip on cell edits or reverts.
1158
+ * - `isChanged` — mutation: cell values differ from the row's origin snapshot.
1159
+ * Can flip back to false if the user types the original value back.
1160
+ * - `isDeleted` — the user marked the row for deletion via the UI.
1161
+ * - `isValid` — the row passes all validation rules.
1162
+ *
1163
+ * Clients route rows to INSERT/UPDATE/DELETE by filtering on these flags; the
1164
+ * SDK does not pre-group them because the routing rules are client-specific.
1165
+ */
1166
+ type ResultRow<TRow extends DataEditorRow = DataEditorRow> = {
1167
+ row: TRow;
1168
+ isNew: boolean;
1169
+ isChanged: boolean;
1170
+ isDeleted: boolean;
1171
+ isValid: boolean;
1172
+ };
1173
+ /**
1174
+ * Per-source bucket of result rows. Pristine backend rows (isNew=false,
1175
+ * isChanged=false, isDeleted=false) are omitted — they are no-ops for the
1176
+ * client. Every other row state is emitted, including pristine imports
1177
+ * (isNew=true, isChanged=false) which the client will INSERT.
1178
+ */
1179
+ type DataEditorSourceResult<TRow extends DataEditorRow = DataEditorRow> = {
1180
+ sourceId: DataSourceId;
1181
+ sourceName: string;
1182
+ rows: ResultRow<TRow>[];
1183
+ };
1184
+ /**
1185
+ * The result object passed to `onComplete` when the user submits changes.
1186
+ * Grouped by source; each row carries flags for client-side routing.
1187
+ *
1188
+ * @example
1189
+ * ```ts
1190
+ * onComplete={async (result, actions) => {
1191
+ * for (const source of result.sources) {
1192
+ * const inserts = source.rows.filter(r => r.isNew && !r.isDeleted && r.isValid);
1193
+ * const updates = source.rows.filter(r => !r.isNew && r.isChanged && !r.isDeleted && r.isValid);
1194
+ * const deletes = source.rows.filter(r => r.isDeleted && !r.isNew);
1195
+ * await persist(source.sourceId, { inserts, updates, deletes });
1196
+ * }
1197
+ * actions.reset();
1198
+ * }}
1199
+ * ```
1200
+ */
1201
+ type DataEditorResult<TRow extends DataEditorRow = DataEditorRow> = {
1202
+ sources: DataEditorSourceResult<TRow>[];
1203
+ counts: {
1204
+ new: number;
1205
+ changed: number;
1206
+ deleted: number;
1207
+ invalid: number;
1208
+ };
1209
+ };
1210
+ type ChunkSourceOptions = {
1211
+ /** Display name for this data source. */
1212
+ source: string;
1213
+ /** Stable identifier. Defaults to `source` value when omitted. */
1214
+ id?: string;
1215
+ /** Can the user delete this source? @default false */
1216
+ deletable?: boolean;
1217
+ /** Marks this source as finished loading. */
1218
+ done?: boolean;
1219
+ };
1220
+ type DataSourceId = string;
1221
+ /** Per-column data passed to `onValueMatch`: the unique imported values and the allowed select options. */
1222
+ type ValueMatchInput = {
1223
+ importedValues: string[];
1224
+ options: string[];
1225
+ };
1226
+ /** Per-column result from `onValueMatch`: maps each imported value to an option value, or `null` to skip auto-matching. */
1227
+ type ValueMatchOutput = Record<string, Record<string, string | null>>;
1228
+ /** File formats supported for import and export. */
1229
+ type DataEditorFormat = "csv" | "tsv" | "xlsx" | "json" | "xml";
1230
+ /**
1231
+ * A client-defined remote data source (e.g. Google Sheets, S3, Dropbox).
1232
+ *
1233
+ * The SDK renders a button per source and calls `fetch()` when clicked.
1234
+ * The client owns all integration complexity — auth, pickers, downloads.
1235
+ *
1236
+ * @example
1237
+ * ```ts
1238
+ * const googleSheets: RemoteSource = {
1239
+ * id: "google-sheets",
1240
+ * label: "Google Sheets",
1241
+ * icon: "<svg>...</svg>",
1242
+ * fetch: async () => {
1243
+ * const data = await myGoogleSheetsLib.pick();
1244
+ * return data.rows; // Record<string, unknown>[]
1245
+ * },
1246
+ * };
1247
+ * ```
1248
+ */
1249
+ type RemoteSource = {
1250
+ /** Unique identifier for this source. */
1251
+ id: string;
1252
+ /** Button label shown to the user. */
1253
+ label: string;
1254
+ /** SVG markup or image URL for the button icon. */
1255
+ icon: string;
1256
+ /** Optional tooltip text. */
1257
+ description?: string;
1258
+ /** Returns a File (parsed via CSV/XLSX/JSON/XML pipeline) or structured records (used directly). */
1259
+ fetch: () => Promise<File | Record<string, unknown>[]>;
1260
+ };
1261
+ /**
1262
+ * Controls the initial view when the editor opens.
1263
+ *
1264
+ * - `"editor"` — opens directly into the spreadsheet grid.
1265
+ * - `"uploader"` — opens the file import wizard first, then transitions to the grid.
1266
+ */
1267
+ type DataEditorVariant = "editor" | "uploader";
1268
+ /** Base configuration shared by the public `DataEditorProps` and the internal provider. */
1269
+ type DataEditorBaseProps<TRow extends DataEditorRow = DataEditorRow> = {
1270
+ /** Column definitions. Each entry describes one column in the grid. */
1271
+ columns: DataEditorColumn[];
1272
+ /**
1273
+ * The column ID used to uniquely identify each row (e.g. `"id"` or `"employeeId"`).
1274
+ * Used to match imported rows to existing data and to track edits.
1275
+ */
1276
+ primaryKey: keyof TRow;
1277
+ /**
1278
+ * Async function that feeds data into the editor. Called once when the editor opens.
1279
+ * Call `onChunk` one or more times to stream rows in batches.
1280
+ * The editor processes each chunk without blocking the UI.
1281
+ *
1282
+ * Pass `options` to tag chunks with a data source. Sources are auto-registered
1283
+ * on first encounter. Omit `options` to send rows to "Existing Data".
1284
+ *
1285
+ * @example
1286
+ * ```ts
1287
+ * // Single source
1288
+ * loadData={async (onChunk) => {
1289
+ * const rows = await fetch("/api/employees").then(r => r.json());
1290
+ * onChunk(rows);
1291
+ * }}
1292
+ *
1293
+ * // Multi-source
1294
+ * loadData={async (onChunk) => {
1295
+ * const sf = await fetchSalesforce();
1296
+ * onChunk(sf, { source: "Salesforce", done: true });
1297
+ *
1298
+ * const hs = await fetchHubSpot();
1299
+ * onChunk(hs, { source: "HubSpot", deletable: true, done: true });
1300
+ * }}
1301
+ * ```
1302
+ */
1303
+ loadData?: (onChunk: (rows: TRow[], options?: ChunkSourceOptions) => void) => Promise<void>;
1304
+ /**
1305
+ * Server-delegated mode. When provided, the SDK acts as a rendering head
1306
+ * and delegates data operations (fetching, filtering, sorting, mutations)
1307
+ * to the client's backend via callbacks.
1308
+ */
1309
+ server?: DataEditorServer<TRow>;
1310
+ /**
1311
+ * Called when the user clicks "Submit". Receives the edited data grouped
1312
+ * by source. Use `actions.reset()` to clear changes after a successful save.
1313
+ *
1314
+ * @example
1315
+ * ```ts
1316
+ * onComplete={async (result, actions) => {
1317
+ * for (const source of result.sources) {
1318
+ * const inserts = source.rows.filter(r => r.isNew && !r.isDeleted && r.isValid);
1319
+ * const updates = source.rows.filter(r => !r.isNew && r.isChanged && !r.isDeleted && r.isValid);
1320
+ * const deletes = source.rows.filter(r => r.isDeleted && !r.isNew);
1321
+ * await persist(source.sourceId, { inserts, updates, deletes });
1322
+ * }
1323
+ * actions.reset();
1324
+ * }}
1325
+ * ```
1326
+ */
1327
+ onComplete?: (result: DataEditorResult<TRow>, actions: DataEditorActions) => void;
1328
+ /** Controls the initial view. `"editor"` opens the grid, `"uploader"` opens the import wizard first. @default "editor" */
1329
+ variant?: DataEditorVariant;
1330
+ /** Override any UI string. Pass a partial object — only the keys you provide are replaced. */
1331
+ translations?: DataEditorTranslations;
1332
+ /** BCP 47 locale tag (e.g. `"en"`, `"fr"`, `"ar"`). Used for plural rules and locale-aware features. @default "en" */
1333
+ locale?: string;
1334
+ /**
1335
+ * Controls row deletion via the right-click context menu.
1336
+ * - `false` — deletion disabled (default).
1337
+ * - `"new"` — only manually added or imported rows can be deleted.
1338
+ * - `"all"` — any row can be deleted.
1339
+ * @default false
1340
+ */
1341
+ enableDeleteRow?: "all" | "new" | false;
1342
+ /** Show the "Add row" button in the data sources panel. @default true */
1343
+ enableAddRow?: boolean;
1344
+ /** Which file formats the user can import. `undefined` allows all, `[]` disables import entirely. */
1345
+ importFormats?: DataEditorFormat[];
1346
+ /** Client-defined remote data sources rendered as buttons on the upload step. */
1347
+ remoteSources?: RemoteSource[];
1348
+ /** Which file formats the user can export. `undefined` allows all, `[]` disables export entirely. */
1349
+ exportFormats?: DataEditorFormat[];
1350
+ /** Row height in pixels. @default 34 */
1351
+ rowHeight?: number;
1352
+ /** Header row height in pixels. @default 36 */
1353
+ headerHeight?: number;
1354
+ /** When `true`, hides all editing UI (submit, add row, delete, import). The grid becomes view-only. */
1355
+ readonly?: boolean;
1356
+ /** Enable right-to-left layout for RTL languages. @default false */
1357
+ rtl?: boolean;
1358
+ /** Allow creating new columns for unmatched CSV headers during import. @default true */
1359
+ enableCreateColumn?: boolean;
1360
+ /** Override column matching during import. Return a map of `{ csvHeader: columnId | null }`. Unmapped or `null` entries fall back to built-in matching. */
1361
+ onColumnMatch?: (headers: string[], columns: DataEditorColumn[]) => Record<string, string | null> | Promise<Record<string, string | null>>;
1362
+ /**
1363
+ * Override value matching during import for select columns. Called once after column mapping
1364
+ * is established with all select columns' imported values and allowed options. Return a map
1365
+ * of `{ columnId: { importedValue: optionValue | null } }`. `null` entries skip auto-matching
1366
+ * for that value. Unmapped values fall back to built-in fuzzy matching.
1367
+ *
1368
+ * @example
1369
+ * ```ts
1370
+ * onValueMatch={async (valuesToMatch) => {
1371
+ * // valuesToMatch = { country: { importedValues: ["espana", "fr"], options: ["Spain", "France", ...] } }
1372
+ * return {
1373
+ * country: { espana: "Spain", fr: "France" },
1374
+ * };
1375
+ * }}
1376
+ * ```
1377
+ */
1378
+ onValueMatch?: (valuesToMatch: Record<string, ValueMatchInput>) => ValueMatchOutput | Promise<ValueMatchOutput>;
1379
+ /** Extra synonyms for column auto-matching, e.g. `{ productsku: ["sku", "articleno"] }`. */
1380
+ synonyms?: Record<string, string[]>;
1381
+ /** Sample rows included in the "Download Example" file. When omitted, a generic row is auto-generated from column config. */
1382
+ sampleData?: Record<string, unknown>[];
1383
+ /** Bring Your Own AI chat. When provided, a chat panel is shown alongside the grid. */
1384
+ chat?: DataEditorChat<TRow>;
1385
+ };
1386
+ /**
1387
+ * Controls what the editor stores in `localStorage`.
1388
+ * Set to `false` to disable all local storage usage.
1389
+ */
1390
+ type DataEditorLocalStorage = false | {
1391
+ /** Cache the license validation result to skip re-validation on reload. @default true */
1392
+ licenseGrant?: boolean;
1393
+ };
1394
+ /** Shared props present in both modal and inline modes. */
1395
+ type DataEditorCommonProps<TRow extends DataEditorRow = DataEditorRow> = DataEditorBaseProps<TRow> & {
1396
+ /** Your Updog license key. Validates on each open. */
1397
+ apiKey: string;
1398
+ /** Controls what the editor stores in `localStorage`. Set to `false` to disable. */
1399
+ localStorage?: DataEditorLocalStorage;
1400
+ /** CSS class added to the wrapper element. */
1401
+ className?: string;
1402
+ /** Called when the SDK catches an internal error. Use for logging, Sentry, etc. Client mode only. */
1403
+ onError?: (error: UpdogError) => void;
1404
+ };
1405
+ /**
1406
+ * Props for modal mode (default). The editor renders as a full-screen dialog
1407
+ * controlled by `open` / `onClose`.
1408
+ */
1409
+ type DataEditorModalProps<TRow extends DataEditorRow = DataEditorRow> = DataEditorCommonProps<TRow> & {
1410
+ /** Rendering mode. @default "modal" */
1411
+ mode?: "modal";
1412
+ /** When `true`, the editor modal is visible. This is a controlled prop. Required in modal mode. */
1413
+ open: boolean;
1414
+ /** Called when the user closes the modal (clicks X or presses Escape). Required in modal mode. */
1415
+ onClose: () => void;
1416
+ };
1417
+ /**
1418
+ * Props for inline mode. The editor renders directly in the DOM with no
1419
+ * modal overlay or close button.
1420
+ */
1421
+ type DataEditorInlineProps<TRow extends DataEditorRow = DataEditorRow> = DataEditorCommonProps<TRow> & {
1422
+ /** Rendering mode. Must be `"inline"` to render without a modal. */
1423
+ mode: "inline";
1424
+ /** Not applicable in inline mode. */
1425
+ open?: never;
1426
+ /** Not applicable in inline mode. */
1427
+ onClose?: never;
1428
+ };
1429
+ /**
1430
+ * Props for the `<DataEditor />` component.
1431
+ *
1432
+ * Supports two rendering modes:
1433
+ * - **Modal** (default): full-screen dialog controlled by `open` / `onClose`.
1434
+ * - **Inline**: renders directly in the DOM, no overlay.
1435
+ *
1436
+ * @example
1437
+ * ```tsx
1438
+ * // Modal mode (default)
1439
+ * <DataEditor
1440
+ * apiKey="your-license-key"
1441
+ * open={isOpen}
1442
+ * onClose={() => setIsOpen(false)}
1443
+ * columns={columns}
1444
+ * primaryKey="id"
1445
+ * loadData={async (onChunk) => onChunk(await fetchRows())}
1446
+ * onComplete={(result, actions) => { saveChanges(result); actions.reset(); }}
1447
+ * />
1448
+ *
1449
+ * // Inline mode
1450
+ * <DataEditor
1451
+ * mode="inline"
1452
+ * apiKey="your-license-key"
1453
+ * columns={columns}
1454
+ * primaryKey="id"
1455
+ * loadData={async (onChunk) => onChunk(await fetchRows())}
1456
+ * onComplete={(result, actions) => { saveChanges(result); actions.reset(); }}
1457
+ * />
1458
+ * ```
1459
+ */
1460
+ type DataEditorProps<TRow extends DataEditorRow = DataEditorRow> = DataEditorModalProps<TRow> | DataEditorInlineProps<TRow>;
1461
+
1462
+ /**
1463
+ * Creates a validator that rejects empty values.
1464
+ *
1465
+ * @param message - Error message shown when the cell is empty.
1466
+ *
1467
+ * @example
1468
+ * ```ts
1469
+ * const columns = [
1470
+ * { id: "name", title: "Name", validate: [required("Name is required")] },
1471
+ * ];
1472
+ * ```
1473
+ */
1474
+ declare function required(message: string): CellValidator;
1475
+ /**
1476
+ * Creates a validator that rejects non-numeric values.
1477
+ *
1478
+ * @param message - Error message shown when the value is not a valid number.
1479
+ */
1480
+ declare function numeric(message: string): CellValidator;
1481
+ /**
1482
+ * Creates a validator that rejects invalid email addresses.
1483
+ *
1484
+ * @param message - Error message shown when the value is not a valid email.
1485
+ */
1486
+ declare function email(message: string): CellValidator;
1487
+ /**
1488
+ * Creates a validator that rejects values not matching `YYYY-MM-DD` or `DD/MM/YYYY`.
1489
+ *
1490
+ * @param message - Error message shown when the value is not a valid date.
1491
+ */
1492
+ declare function date(message: string): CellValidator;
1493
+ /**
1494
+ * Creates a validator that ensures this date is on or after the date in another column.
1495
+ *
1496
+ * @param startDateField - Column ID of the start-date field to compare against.
1497
+ * @param message - Error message shown when the end date is before the start date.
1498
+ *
1499
+ * @example
1500
+ * ```ts
1501
+ * const columns = [
1502
+ * { id: "startDate", title: "Start", dependentFields: ["endDate"] },
1503
+ * { id: "endDate", title: "End", validate: [endDateAfterStart("startDate", "End must be after start")] },
1504
+ * ];
1505
+ * ```
1506
+ */
1507
+ /**
1508
+ * Creates a validator that rejects values not in the allowed set.
1509
+ *
1510
+ * @param values - Array of allowed values (matched by strict equality).
1511
+ * @param message - Error message shown when the value is not in the set.
1512
+ *
1513
+ * @example
1514
+ * ```ts
1515
+ * const columns = [
1516
+ * { id: "status", title: "Status", validate: [oneOf(["active", "inactive"], "Invalid status")] },
1517
+ * ];
1518
+ * ```
1519
+ */
1520
+ declare function oneOf(values: string[], message: string): CellValidator;
1521
+ declare function endDateAfterStart(startDateField: string, message: string): CellValidator;
1522
+
1523
+ type Props = DataEditorProps;
1524
+ /**
1525
+ * All properties available on the `<updog-editor>` element.
1526
+ *
1527
+ * Set simple values as HTML attributes (`api-key`, `open`, `readonly`, `rtl`, `locale`, `variant`, `primary-key`).
1528
+ * Set complex values (objects, functions, arrays) as JS properties or via `configure()`.
1529
+ *
1530
+ * Closing is handled via the `"close"` event — listen for it and call `editor.hide()`.
1531
+ *
1532
+ * @example
1533
+ * ```ts
1534
+ * const editor = document.querySelector("updog-editor")!;
1535
+ *
1536
+ * editor.apiKey = "your-license-key";
1537
+ * editor.primaryKey = "id";
1538
+ * editor.columns = [
1539
+ * { id: "name", title: "Name", width: 200 },
1540
+ * { id: "email", title: "Email", width: 250 },
1541
+ * ];
1542
+ *
1543
+ * editor.loadData = async (onChunk) => {
1544
+ * const rows = await fetch("/api/employees").then(r => r.json());
1545
+ * onChunk(rows);
1546
+ * };
1547
+ *
1548
+ * editor.onComplete = (result, actions) => {
1549
+ * console.log("Valid rows:", result.valid);
1550
+ * actions.reset();
1551
+ * };
1552
+ *
1553
+ * editor.addEventListener("close", () => editor.hide());
1554
+ * editor.show();
1555
+ * ```
1556
+ */
1557
+ type UpdogEditorProps = Omit<Props, "onClose" | "className"> & {
1558
+ /**
1559
+ * Set multiple properties at once. Useful for initial setup.
1560
+ *
1561
+ * @example
1562
+ * ```ts
1563
+ * editor.configure({
1564
+ * apiKey: "your-key",
1565
+ * columns: [...],
1566
+ * primaryKey: "id",
1567
+ * loadData: async (onChunk) => onChunk(await fetchRows()),
1568
+ * });
1569
+ * ```
1570
+ */
1571
+ configure(props: Partial<Props>): void;
1572
+ /** Open the editor modal. Same as setting `editor.open = true`. */
1573
+ show(): void;
1574
+ /** Close the editor modal. Same as setting `editor.open = false`. */
1575
+ hide(): void;
1576
+ };
1577
+ declare global {
1578
+ /**
1579
+ * The `<updog-editor>` custom element. A full-screen modal spreadsheet editor.
1580
+ *
1581
+ * Supports CSV/XLSX import, cell validation, undo/redo, find & replace, and exporting.
1582
+ * Works in any framework (Angular, Vue, vanilla JS) — no React needed.
1583
+ *
1584
+ * @see {@link UpdogEditorProps} for all available properties.
1585
+ */
1586
+ interface UpdogEditorElement extends HTMLElement, UpdogEditorProps {
1587
+ }
1588
+ interface HTMLElementTagNameMap {
1589
+ "updog-editor": UpdogEditorElement;
1590
+ }
1591
+ }
1592
+
1593
+ export { date, email, endDateAfterStart, numeric, oneOf, required };