@updog/data-editor 0.1.10 → 0.1.11

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.
Files changed (3) hide show
  1. package/index.d.ts +1792 -1787
  2. package/index.js +2672 -2444
  3. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -372,37 +372,63 @@ declare var export_default = {
372
372
  redo: "Redo",
373
373
  },
374
374
  validation: {
375
- required: "This field is required",
376
- invalidNumber: "Invalid number",
377
- invalidEmail: "Invalid email address",
378
- invalidDate: "Invalid date",
379
375
  endDateBeforeStart: "End date must be after start date",
376
+ invalidDate: "Invalid date",
377
+ invalidEmail: "Invalid email address",
378
+ invalidFormat: "Invalid format",
379
+ invalidNumber: "Invalid number",
380
+ invalidOption: "Invalid option",
381
+ outOfRange: "Out of range",
382
+ required: "This field is required",
380
383
  valueMustBeUnique: "Value must be unique",
381
384
  },
382
385
  license: {
383
386
  loading: "Loading data editor",
384
- errors: {
385
- LICENSE_INVALID: {
387
+ },
388
+ connecting: {
389
+ label: "Connecting to server…",
390
+ },
391
+ errors: {
392
+ RENDER_ERROR: {
393
+ title: "Something went wrong",
394
+ text: "Please close this window and try again",
395
+ },
396
+ license: {
397
+ invalid: {
386
398
  title: "Invalid license",
387
399
  text: "Please contact support for assistance.",
388
400
  },
389
- MISSING_API_KEY: {
401
+ missing: {
390
402
  title: "Missing API key",
391
403
  text: "No API key was provided. Please configure a valid API key.",
392
404
  },
393
- DOMAIN_NOT_ALLOWED: {
405
+ domain_not_allowed: {
394
406
  title: "Domain not allowed",
395
407
  text: "This domain is not authorized for this license. Please check your domain settings.",
396
408
  },
397
- SUBSCRIPTION_INACTIVE: {
409
+ subscription_inactive: {
398
410
  title: "Subscription inactive",
399
411
  text: "Your subscription is no longer active. Please renew to continue.",
400
412
  },
401
413
  },
402
- },
403
- errorState: {
404
- title: "Something went wrong",
405
- text: "Please close this window and try again",
414
+ scale: {
415
+ bootstrap_failed: {
416
+ title: "Couldn't connect to the server",
417
+ text: "The server returned an error. Please refresh the page or contact support.",
418
+ },
419
+ workspace_lost: {
420
+ title: "Connection lost",
421
+ text: "The session ended unexpectedly. Refresh the page to reconnect.",
422
+ },
423
+ unreachable: {
424
+ title: "Server unreachable",
425
+ text: "Check your connection and refresh the page.",
426
+ },
427
+ server_error: {
428
+ title: "Server error",
429
+ text: "The server returned an error. Please refresh the page or contact support.",
430
+ },
431
+ },
406
432
  },
407
433
  uploader: {
408
434
  steps: {
@@ -519,1100 +545,1702 @@ type ExpandPluralKeys<T> = T extends Record<string, unknown>
519
545
  type DataEditorTranslations = DeepPartial<ExpandPluralKeys<typeof export_default>>;
520
546
 
521
547
  /**
522
- * ChunkedProcessor Generic utility for processing items in prioritized chunks
523
- * with cancellation support via generation counter.
548
+ * Base row shape. Each key is a column ID, each value is the cell data.
549
+ * Extend this with your own type via the `<DataEditor<TRow>>` generic for
550
+ * type-safe column access. When the generic is omitted, rows are typed as
551
+ * `Record<string, unknown>`.
524
552
  *
525
- * Used to break O(N) synchronous operations into non-blocking background work:
526
- * 1. Priority items are processed synchronously first (e.g. viewport rows)
527
- * 2. Remaining items are processed in chunks via MessageChannel
528
- * 3. Between chunks the browser can paint frames and handle events
553
+ * @example
554
+ * ```ts
555
+ * type Employee = { id: string; name: string; email: string };
556
+ * <DataEditor<Employee> columns={...} primaryKey="id" />
557
+ * ```
558
+ */
559
+ type DataEditorRow = Record<string, unknown>;
560
+ /** Sort direction. */
561
+ type SortDirection = "asc" | "desc";
562
+ /** Current sort state. `null` means no active sort. */
563
+ type SortState = {
564
+ columnId: string;
565
+ direction: SortDirection;
566
+ } | null;
567
+ type Filters = {
568
+ search: string;
569
+ matchCase: boolean;
570
+ matchEntireCell: boolean;
571
+ errorMessageFilters: string[];
572
+ showOnlyNewRows: boolean;
573
+ showOnlyEditedRows: boolean;
574
+ showOnlyEmptyCells: boolean;
575
+ /** When true, show only rows flagged for deletion (bin mode). All other filters are bypassed. */
576
+ showOnlyDeletedRows: boolean;
577
+ filterColumns: string[] | null;
578
+ /** Per-column value filters. Key = column ID, value = allowed display-formatted strings. */
579
+ columnValueFilters: Record<string, string[]>;
580
+ /** Per-column numeric range filters. Key = column ID. */
581
+ columnRangeFilters: Record<string, {
582
+ min?: number;
583
+ max?: number;
584
+ }>;
585
+ /** Per-column date range filters. Key = column ID, values are ISO date strings (YYYY-MM-DD). */
586
+ columnDateRangeFilters: Record<string, {
587
+ min?: string;
588
+ max?: string;
589
+ }>;
590
+ };
591
+
592
+ /**
593
+ * A built-in declarative validator. Mode-symmetric: the SDK ships a TS
594
+ * interpreter, the Updog Scale Go binary ships a Go interpreter, and both
595
+ * agree on pass/fail outcomes per `api/validators.json`.
596
+ */
597
+ type BuiltInValidator = {
598
+ type: "required";
599
+ message?: string;
600
+ } | {
601
+ type: "regex";
602
+ pattern: string;
603
+ flags?: string;
604
+ message?: string;
605
+ } | {
606
+ type: "oneOf";
607
+ values: string[];
608
+ message?: string;
609
+ } | {
610
+ type: "range";
611
+ min?: number;
612
+ max?: number;
613
+ message?: string;
614
+ } | {
615
+ type: "email";
616
+ message?: string;
617
+ } | {
618
+ type: "date";
619
+ format?: "YYYY-MM-DD" | "DD/MM/YYYY";
620
+ message?: string;
621
+ } | {
622
+ type: "numeric";
623
+ message?: string;
624
+ };
625
+ /**
626
+ * Server-mode-only escape hatch: an expression evaluated by the Go binary
627
+ * via expr-lang. Skipped at runtime in client mode (warned at mount).
628
+ */
629
+ type ExpressionValidator = {
630
+ type: "expression";
631
+ expr: string;
632
+ message?: string;
633
+ };
634
+ /**
635
+ * Client-mode-only escape hatch: a JS predicate run inline. Dropped (with
636
+ * one warn per column) when the SDK serializes the schema for server mode.
637
+ */
638
+ type FunctionValidator = {
639
+ type: "function";
640
+ fn: CellValidator;
641
+ };
642
+ /**
643
+ * The validator-rule union accepted by `DataEditorColumn.validators`.
644
+ * - Built-ins and `expression` are declarative objects (cross the wire).
645
+ * - `function` wraps a `CellValidator`; client-mode-only, dropped+warned in server mode.
529
646
  *
530
- * Uses MessageChannel instead of setTimeout(0) to avoid the browser's ~4ms
531
- * minimum delay imposed on nested setTimeouts, reducing scheduling overhead
532
- * from ~6ms/chunk to ~1ms/chunk.
647
+ * Named `ValidatorRule` (not `Validator`) so it doesn't clash with the
648
+ * runtime `Validator` class in `core/Validator.ts`.
649
+ */
650
+ type ValidatorRule = BuiltInValidator | ExpressionValidator | FunctionValidator;
651
+
652
+ /** Severity level for a validation message. */
653
+ type ValidationLevel = "error";
654
+ /**
655
+ * A single validation message attached to a cell.
656
+ * Return this from a `CellValidator` to flag a problem.
657
+ */
658
+ type ValidationError = {
659
+ level: ValidationLevel;
660
+ /** Human-readable message shown in the cell tooltip. */
661
+ message: string;
662
+ };
663
+ type ValidationResult = ValidationError[] | null;
664
+ /**
665
+ * A function that validates a single cell value.
666
+ * Return a `ValidationError` to flag a problem, or `null` if the value is valid.
533
667
  *
534
- * Cancellation uses the same generation counter pattern as FilterEngine:
535
- * calling cancel() or run() again increments the generation, and any
536
- * in-flight MessageChannel callbacks silently exit when they detect
537
- * their generation is stale.
668
+ * A `ValidationError` with `level: "error"` flags the cell in the grid but
669
+ * does not block submission invalid rows are delivered to `onComplete`
670
+ * alongside valid ones, tagged via the `isValid` flag.
538
671
  *
539
- * This class has zero domain knowledge it works with any item type.
672
+ * To use a `CellValidator` in the `validators` array, wrap it as
673
+ * `{ type: "function", fn: (value, row) => ... }`. For declarative rules,
674
+ * pass a built-in object literal — e.g. `{ type: "required" }`,
675
+ * `{ type: "email" }`. See `ValidatorRule` for the full union.
676
+ *
677
+ * @param value - The current cell value.
678
+ * @param row - The full row, useful for cross-field checks.
540
679
  */
541
- type ChunkedProcessorCallbacks<T> = {
542
- /** All items to process (excluding priority items). */
543
- all: T[];
544
- /** Items processed synchronously before any chunking begins. */
545
- priority: T[];
546
- /** Called for each batch of items (priority batch first, then chunks). */
547
- onChunk: (items: T[]) => void;
548
- /** Called after each chunk with (processed so far, total). */
549
- onProgress: (processed: number, total: number) => void;
550
- /** Called once when all items have been processed. */
551
- onComplete: () => void;
552
- /** Called when processing is cancelled via cancel(). */
553
- onCancel: () => void;
680
+ type CellValidator = (value: unknown, row: DataEditorRow) => ValidationError | null;
681
+ /** Text input cell. This is the default editor when no `editor` is specified. */
682
+ type TextEditorCell = {
683
+ type: "text";
554
684
  };
555
- declare class ChunkedProcessor<T> {
556
- private _generation;
557
- private _isRunning;
558
- private _chunkSize;
559
- private _activeOnCancel;
560
- private _channel;
561
- private _pendingChunkFn;
562
- constructor(chunkSize?: number);
563
- get isRunning(): boolean;
564
- get chunkSize(): number;
565
- run(params: ChunkedProcessorCallbacks<T>): void;
566
- cancel(): void;
567
- }
568
-
569
- type RegisterSourceOptions = {
570
- name: string;
571
- id?: DataSourceId;
572
- isDeletable?: boolean;
573
- isInitialData?: boolean;
685
+ /** Date picker cell. Optionally restrict the selectable date range. */
686
+ type DateEditorCell = {
687
+ type: "date";
688
+ /** Earliest selectable date. */
689
+ minDate?: Date;
690
+ /** Latest selectable date. */
691
+ maxDate?: Date;
574
692
  };
575
- type MergeEntry<TRow> = {
576
- row: TRow;
577
- sourceId: DataSourceId;
578
- isNew: boolean;
579
- isEdited: boolean;
693
+ /** Dropdown select cell. The user picks from a fixed list of options. */
694
+ type SelectEditorCell = {
695
+ type: "select";
696
+ /** The list of options shown in the dropdown. Each string is both the stored value and the display label. */
697
+ options: string[];
580
698
  };
581
- type RemovalPlan<TRow> = {
582
- rowsToDelete: Set<TRowId>;
583
- rowsToRestore: Array<{
584
- rowId: TRowId;
585
- row: TRow;
586
- originalSourceId: DataSourceId;
587
- isNew: boolean;
588
- isEdited: boolean;
589
- }>;
699
+ /** Number input cell with locale-aware formatting. */
700
+ type NumberEditorCell = {
701
+ type: "number";
702
+ /** Maximum number of decimal digits allowed. When omitted, decimals are unrestricted. */
703
+ decimalPlaces?: number;
704
+ /** Character used as the decimal point (e.g. `"."` or `","`). Defaults to the browser locale. */
705
+ decimalSeparator?: string;
706
+ /** Character inserted between groups of three digits (e.g. `","` or `"."`). Defaults to the browser locale. */
707
+ thousandsSeparator?: string;
708
+ /** Extra characters to allow beyond digits, decimal separator, and minus sign (e.g. `"%-"`). When defined, minus is only kept if explicitly included. */
709
+ allowChars?: string;
590
710
  };
591
- type ExtendedRemovalPlan<TRow> = RemovalPlan<TRow> & {
592
- sourceId: DataSourceId;
593
- repairedEntries: Array<{
594
- sourceId: DataSourceId;
595
- rowId: TRowId;
596
- before: MergeEntry<TRow>;
597
- after: MergeEntry<TRow> | null;
598
- }>;
711
+ /**
712
+ * Controls how a cell is edited.
713
+ *
714
+ * - `"text"` — plain text input (default).
715
+ * - `"date"` — date picker with optional min/max bounds.
716
+ * - `"select"` — dropdown with a fixed list of options.
717
+ * - `"number"` — number input with locale-aware formatting.
718
+ */
719
+ type CellEditor = TextEditorCell | DateEditorCell | SelectEditorCell | NumberEditorCell;
720
+ /** Dropdown filter shown in the sidebar Filters panel. */
721
+ type SelectColumnFilter = {
722
+ type: "select";
723
+ /** Label displayed above the filter. */
724
+ label?: string;
725
+ /** Placeholder text when nothing is selected. */
726
+ placeholder?: string;
727
+ /** Fixed list of filter options. When omitted, options are derived from column values. */
728
+ options?: string[];
729
+ /** Allow selecting multiple values at once. */
730
+ multiple?: boolean;
599
731
  };
600
- declare class SourceManager<TRow extends DataEditorRow = DataEditorRow> {
601
- private readonly _defaultSourceId;
602
- private readonly overrides;
603
- private readonly sources;
604
- private readonly mergedRows;
605
- getSourceId(rowId: TRowId): DataSourceId;
606
- setSourceId(rowId: TRowId, sourceId: DataSourceId): void;
607
- deleteSourceId(rowId: TRowId): void;
608
- getOverrides(): ReadonlyMap<TRowId, DataSourceId>;
609
- register(options: RegisterSourceOptions): DataSourceId;
610
- /**
611
- * Re-insert a source using a full captured state, preserving
612
- * isVisible, isLoading, rowCount, etc. Used by SourceLifecycle.restore.
613
- * If the source already exists, overwrites its state.
614
- */
615
- restoreState(state: DataSourceState): void;
616
- has(sourceId: DataSourceId): boolean;
617
- get(sourceId: DataSourceId): DataSourceState | undefined;
618
- delete(sourceId: DataSourceId): void;
619
- setLoading(sourceId: DataSourceId, isLoading: boolean): void;
620
- finalizeAllSources(): void;
621
- values(): IterableIterator<DataSourceState>;
622
- getHiddenSourceIds(): Set<DataSourceId>;
623
- saveMergeSnapshot(sourceId: DataSourceId, rowId: TRowId, existingRow: TRow, previousSourceId: DataSourceId, isNew: boolean, isEdited: boolean): void;
624
- /**
625
- * Public so commands can re-install merge entries during undo of a remove.
626
- */
627
- restoreMergeEntry(sourceId: DataSourceId, rowId: TRowId, entry: MergeEntry<TRow>): void;
628
- /**
629
- * Pure — computes the full removal plan without mutating any state.
630
- * Callers run applyRemovalPlan(plan) to commit.
631
- */
632
- planRemoval(sourceId: DataSourceId): ExtendedRemovalPlan<TRow> | null;
633
- /**
634
- * Mutates internal state per plan produced by planRemoval.
635
- */
636
- applyRemovalPlan(plan: ExtendedRemovalPlan<TRow>): void;
637
- clear(): void;
638
- private getUniqueName;
639
- }
640
-
641
- type ServerDataManagerDeps<TRow extends DataEditorRow = DataEditorRow> = {
642
- clear(): void;
643
- setLoading(isLoading: boolean): void;
644
- registerSource(options: RegisterSourceOptions): DataSourceId;
645
- setSourceLoading(sourceId: DataSourceId, isLoading: boolean): void;
646
- getLocalRowCount(): number;
647
- replaceServerRows(sourceId: DataSourceId, rows: ServerRow<TRow>[], offset: number, counts?: ServerQueryCounts): void;
648
- appendServerRows(rows: ServerRow<TRow>[], counts?: ServerQueryCounts): void;
649
- prependServerRows(rows: ServerRow<TRow>[], offset: number, counts?: ServerQueryCounts): void;
650
- applyServerRowMeta(rows: ServerRow<TRow>[], counts?: ServerQueryCounts): void;
732
+ /** Numeric min/max range filter shown in the sidebar Filters panel. */
733
+ type NumberRangeColumnFilter = {
734
+ type: "number-range";
735
+ /** Label displayed above the filter. */
736
+ label?: string;
651
737
  };
652
- type FetchDirection = "forward" | "backward" | "jump";
653
- declare class ServerDataManager<TRow extends DataEditorRow = DataEditorRow> {
654
- private _offset;
655
- private _totalCount;
656
- private _isFetching;
657
- private readonly _pageSize;
658
- private readonly _maxBufferRows;
659
- private _filters;
660
- private _sources;
661
- private _sort;
662
- private _filterOptions;
663
- private _filterOptionsFetched;
664
- private _abortController;
665
- private _syncAbort;
666
- private _lastVisibleStart;
667
- private _debouncedFetch;
668
- private readonly _config;
669
- private readonly _dataStoreRef;
670
- private readonly _sourceLabel;
671
- private _onChanged;
672
- constructor(config: DataEditorServer<TRow>, dataStoreRef: ServerDataManagerDeps<TRow>, sourceLabel: string);
673
- get offset(): number;
674
- get totalCount(): number | null;
675
- get isFetching(): boolean;
676
- get pageSize(): number;
677
- get maxBufferRows(): number;
678
- setOnChanged(callback: () => void): void;
679
- setOffset(offset: number): void;
680
- setTotalCount(count: number): void;
681
- private setFetching;
682
- getExcess(currentCount: number, newCount: number): number;
683
- shouldFetch(visibleStart: number, visibleEnd: number, loadedCount: number): FetchDirection | null;
684
- /**
685
- * Full reload — abort in-flight, clear store, fetch first page.
686
- * Called on initial load, search/filter/sort changes, and resetFilters.
687
- */
688
- reload(): void;
689
- /**
690
- * Scroll-driven pagination with velocity-based debouncing.
691
- * Called from CanvasGrid on every scroll event.
692
- */
693
- handleScroll(visibleStart: number, visibleEnd: number): void;
694
- /**
695
- * Fetch a single page based on scroll position and current window state.
696
- */
697
- private fetchPage;
698
- /**
699
- * Re-query the current viewport and replace row data, metadata, and counts.
700
- * Called after successful edits and find-and-replace mutations.
701
- * Each call aborts the previous in-flight sync.
702
- */
703
- syncCurrentView(): void;
704
- /**
705
- * Merge filter keys into server filter state and reload.
706
- * Called by DataStore.setFilters() in server mode and by filter components.
707
- */
708
- setFilters(filters: Partial<Filters>): void;
709
- /**
710
- * Set sort state and reload.
711
- */
712
- setSort(sort: SortState): void;
713
- /**
714
- * Restrict query to visible sources and reload.
715
- * Pass `undefined` to include all sources.
716
- */
717
- setSources(sources: string[] | undefined): void;
718
- /**
719
- * Clear all filters and sort, then reload.
720
- */
721
- resetFilters(): void;
722
- /**
723
- * One-time fetch of filter option dictionaries for sidebar filter controls.
724
- */
725
- fetchFilterOptions(): void;
726
- getFilterOptions(): FilterOptionsResponse | null;
727
- get onEdit(): (params: EditParams, options?: ServerCallOptions) => Promise<EditResponse | void>;
728
- get filters(): Record<string, unknown>;
729
- get sort(): SortState;
730
- get sources(): string[] | undefined;
731
- get onSourceRemove(): ((params: SourceRemoveParams) => Promise<void>) | undefined;
732
- get onColumnDelete(): ((params: ColumnDeleteParams) => Promise<EditResponse | void>) | undefined;
733
- get onColumnEdit(): ((params: ColumnEditParams) => Promise<EditResponse | void>) | undefined;
734
- get hasExport(): boolean;
735
- private _exportAbortController;
736
- export(format: DataEditorFormat, allRows: boolean, rtl: boolean): Promise<void>;
737
- clear(): void;
738
- destroy(): void;
739
- }
740
-
741
- /**
742
- * A post-resolution selection rectangle in stable coordinate space.
743
- * Produced by grid-layer resolvers from CellRange[] in grid-index space.
744
- * Consumed by DataStore operations and server sync.
745
- */
746
- type SelectionRect = {
747
- readonly fields: readonly string[];
748
- readonly rowIds: readonly TRowId[];
738
+ /** Date min/max range filter shown in the sidebar Filters panel. */
739
+ type DateRangeColumnFilter = {
740
+ type: "date-range";
741
+ /** Label displayed above the filter. */
742
+ label?: string;
749
743
  };
750
-
751
744
  /**
752
- * ServerEditBuilder Stateless coordinate translator for server-delegated edits.
753
- *
754
- * Converts frontend coordinates (TRowId, column index, grid ranges) into
755
- * `EditParams` with `Region[]` that the server can interpret.
745
+ * Filter control shown in the sidebar Filters panel for a column.
756
746
  *
757
- * Does NOT call `onEdit`. Only builds params.
758
- * DataStore calls the builder, then sends the result to the server.
747
+ * - `"select"` dropdown to pick one or more values.
748
+ * - `"number-range"` two inputs for min and max number.
749
+ * - `"date-range"` — two date pickers for start and end date.
750
+ */
751
+ type ColumnFilter = SelectColumnFilter | NumberRangeColumnFilter | DateRangeColumnFilter;
752
+ /** Lock mode for a column. `"all"` locks for every row; `"default"` locks only default-source rows. */
753
+ type ColumnLockMode = "all" | "default";
754
+ /**
755
+ * Defines a column in the editor grid.
759
756
  *
760
- * Responsibilities:
761
- * - TRowId → ServerRowId translation via primaryKey
762
- * - Grid index column ID translation via columns array
763
- * - Filter/sort context attachment from ServerDataManager
757
+ * @example
758
+ * ```ts
759
+ * const columns: DataEditorColumn[] = [
760
+ * { id: "name", title: "Full Name", size: 200, validators: [{ type: "required", message: "Name is required" }] },
761
+ * { id: "email", title: "Email", size: 250, validators: [{ type: "required", message: "Email is required" }, { type: "email", message: "Invalid email" }], unique: true },
762
+ * { id: "role", title: "Role", editor: { type: "select", options: ["Admin", "Editor", "Viewer"] } },
763
+ * { id: "salary", title: "Salary", validators: [{ type: "numeric", message: "Must be a number" }], formatter: (v) => v ? `$${v}` : "" },
764
+ * ];
765
+ * ```
764
766
  */
765
-
766
- type ServerEditDeps<TRow extends DataEditorRow = DataEditorRow> = {
767
- getPrimaryKey: () => string;
768
- getRowById: (id: TRowId) => TRow | undefined;
769
- getColumnIds: () => string[];
770
- getFilters: () => Record<string, unknown>;
771
- getSort: () => SortState;
772
- getLockedColumns: () => ReadonlyMap<string, ColumnLockMode>;
773
- };
774
- declare class ServerEditBuilder<TRow extends DataEditorRow = DataEditorRow> {
775
- private readonly _deps;
776
- constructor(deps: ServerEditDeps<TRow>);
777
- resolveServerRowId(rowId: TRowId): ServerRowId | undefined;
778
- buildRegion(rowIds: TRowId[], columnIds: string[]): Region;
767
+ type DataEditorColumn = {
768
+ /** Unique column identifier. Must match the keys in your row data. */
769
+ id: string;
770
+ /** Column header text shown to the user. */
771
+ title: string;
779
772
  /**
780
- * Collapse rowIds × columnIds into minimal Region[].
781
- * - All columns omit column fields (row-only regions).
782
- * - Contiguous columns in schema order single fromColumn/toColumn span.
783
- * - Non-contiguous one region per contiguous column group.
784
- * Rows are expressed as fromRow/toRow using first/last of the provided array.
773
+ * One or more validators run on every edit. Accepts:
774
+ * - Built-in object literals: `{ type: "required" | "email" | "regex" | "range" | "oneOf" | "date" | "numeric", ... }`.
775
+ * - `{ type: "expression", expr }` server-mode only. Warned in client mode.
776
+ * - `{ type: "function", fn }` client-mode only. Dropped+warned in server mode.
777
+ *
778
+ * See `_specs/VALIDATIONS.md` §4.5 for the per-mode support matrix.
785
779
  */
786
- buildRegions(rowIds: TRowId[], columnIds: string[]): Region[];
780
+ validators?: ValidatorRule[];
787
781
  /**
788
- * Collapse columnIds into minimal column-only Region[] (all rows implied).
789
- * - All columns `{ allSelected: true }`.
790
- * - Contiguous in schema order → single `{ fromColumn, toColumn }`.
791
- * - Non-contiguous → one region per contiguous group.
782
+ * When `true`, the editor flags duplicate values in this column as errors.
783
+ * The error message is localized via the `translations` prop
784
+ * (`dataEditor.validation.valueMustBeUnique`).
792
785
  */
793
- buildColumnRegions(columnIds: string[]): Region[];
786
+ unique?: boolean;
794
787
  /**
795
- * Build minimal Region[] from multiple selection rectangles.
796
- * Each rect is collapsed independently, preserving disjoint selections.
788
+ * Column IDs to revalidate when this column changes. Use for cross-field
789
+ * rules like "end date must be after start date".
797
790
  */
798
- buildRegionsFromRects(rects: SelectionRect[]): Region[];
799
- buildAllSelectedRegion(): Region;
800
- buildColumnRegion(columnId: string): Region;
801
- buildRowRegion(fromRowId: TRowId, toRowId: TRowId): Region;
802
- cellEdit(rowId: TRowId, field: string, value: unknown): EditParams;
803
- clear(target: Region[]): EditParams;
804
- paste(source: Region[], target: Region[], cut?: boolean): EditParams;
805
- pasteExternal(target: Region[], values: unknown[][]): EditParams;
806
- fill(source: Region[], target: Region[]): EditParams;
807
- transform(target: Region[], transform: TransformParams): EditParams;
808
- deleteRows(rowRanges: [TRowId, TRowId][]): EditParams;
809
- restoreRows(rowRanges: [TRowId, TRowId][]): EditParams;
810
- deleteAllRows(): EditParams;
811
- restoreAllRows(): EditParams;
812
- insertRow(anchorRowId: TRowId | undefined, position: InsertParams["position"], values: unknown[][], columnIds: string[]): EditParams;
791
+ dependentFields?: string[];
792
+ /** Format the display value without changing stored data. E.g. add `$` prefix. */
793
+ formatter?: (value: string) => string;
813
794
  /**
814
- * Returns null when all columns are selected (caller decides representation).
815
- * Otherwise returns contiguous column spans as `{ fromColumn, toColumn }` regions.
795
+ * Transform a value before it enters the store. Runs when rows are uploaded
796
+ * to the data editor.
816
797
  */
817
- private collapseColumns;
818
- private viewContext;
819
- }
820
-
821
- /**
822
- * Categories of internal errors surfaced through the `onError` callback.
823
- *
824
- * - `PARSE_ERROR` file parsing failed (corrupt CSV, invalid XLSX, etc.).
825
- * - `RENDER_ERROR` — component render failed (caught by error boundary).
826
- * - `TRANSFORM_ERROR` — data transformation failed (column transform, value mapping).
827
- * - `VALIDATION_ERROR` validation execution failed (validator threw instead of returning).
828
- * - `WORKER_ERROR` — Web Worker failed (filter worker, chat transform worker).
829
- * - `COMMAND_ERROR` — undo/redo command failed.
830
- * - `OPERATION_ERROR` general operation failed (bulk mutations, imports).
831
- */
832
- type UpdogErrorCode = "PARSE_ERROR" | "RENDER_ERROR" | "TRANSFORM_ERROR" | "VALIDATION_ERROR" | "WORKER_ERROR" | "COMMAND_ERROR" | "OPERATION_ERROR";
833
- /**
834
- * An internal error caught by the SDK and passed to `onError`. The SDK
835
- * recovers gracefully where possible — `onError` is for your logging and
836
- * monitoring (Sentry, Datadog, etc.).
837
- *
838
- * @example
839
- * ```ts
840
- * onError={(error) => {
841
- * Sentry.captureException(error.originalError ?? error, {
842
- * tags: { code: error.code, source: error.source },
843
- * });
844
- * }}
845
- * ```
846
- */
847
- type UpdogError = {
848
- /** The error category. */
849
- code: UpdogErrorCode;
850
- /** Human-readable description. */
851
- message: string;
852
- /** Module or subsystem that raised the error. */
853
- source: string;
854
- /** The underlying thrown value, when available. */
855
- originalError?: unknown;
798
+ transformer?: (value: unknown) => unknown;
799
+ /** How the cell is edited. Defaults to text input. */
800
+ editor?: CellEditor;
801
+ /** Adds a filter control for this column in the sidebar Filters panel. */
802
+ filter?: ColumnFilter;
803
+ /**
804
+ * Whether this column can be pinned to the left (right in RTL) via the
805
+ * header context menu. @default true
806
+ */
807
+ pinnable?: boolean;
808
+ /** Column width in pixels. @default 150 */
809
+ size?: number;
810
+ /**
811
+ * Controls whether cells in this column are locked. A column locked via
812
+ * configuration cannot be unlocked from the UI.
813
+ * - `true` | `"all"` locked for every row.
814
+ * - `"default"` — locked only for default-source rows; rows added manually,
815
+ * duplicated, or imported remain editable.
816
+ * - `false` | `undefined` not locked.
817
+ */
818
+ locked?: boolean | ColumnLockMode;
856
819
  };
857
820
 
858
- declare class ErrorHandler {
859
- private onError?;
860
- constructor(onError?: (error: UpdogError) => void);
861
- handleError(error: UpdogError): void;
862
- }
863
-
821
+ /** Params passed to `findAndReplace.onFind` when the user types a search query. */
822
+ type FindParams = {
823
+ /** The search string. */
824
+ search: string;
825
+ /** When `true`, matching is case-sensitive. */
826
+ matchCase?: boolean;
827
+ /** When `true`, the entire cell value must equal the search string. */
828
+ matchEntireCell?: boolean;
829
+ /** Restrict search to these columns. `null` or omitted = all columns. */
830
+ columnIds?: string[] | null;
831
+ /** Current view filters so the server can scope matches to the active filter set. */
832
+ filters?: QueryFilters;
833
+ /** Current sort state so match ordering follows the visual row order. */
834
+ sort?: SortState;
835
+ };
836
+ /** A single match location returned by the server. */
837
+ type FindMatch = {
838
+ /** Row position in the current filtered+sorted view (for grid scrolling). */
839
+ rowIndex: number;
840
+ /** Column ID where the match occurs. */
841
+ columnId: string;
842
+ /** Character offset within the cell value (for inline highlight). */
843
+ startIndex: number;
844
+ /** 0-based position in the ordered match list (for counter display). */
845
+ matchIndex: number;
846
+ };
847
+ /** Response from `findAndReplace.onFind`. */
848
+ type FindResponse = {
849
+ /** Total number of matches across all rows. */
850
+ totalCount: number;
851
+ /** The first match. Omit when `totalCount` is 0. */
852
+ current?: FindMatch;
853
+ };
854
+ /** Params passed to `findAndReplace.onNavigate` when the user clicks prev/next. */
855
+ type FindNavigateParams = FindParams & {
856
+ /** Navigation direction. */
857
+ direction: "next" | "prev";
858
+ /** Current match position so the server knows where to navigate from. */
859
+ currentMatchIndex: number;
860
+ };
861
+ /** Params passed to `findAndReplace.onReplace`. */
862
+ type ReplaceParams = FindParams & {
863
+ /** The replacement text. */
864
+ replacement: string;
865
+ /** When `true`, replace all matches. When omitted or `false`, replace only `target`. */
866
+ all?: boolean;
867
+ /** The specific match to replace. Required when `all` is not `true`. */
868
+ target?: FindMatch;
869
+ };
870
+ /** Response from `findAndReplace.onReplace`. */
871
+ type ReplaceResponse = {
872
+ /** Remaining match count after replacement. */
873
+ totalCount: number;
874
+ /** Next match to navigate to after replacement. Omit when none left. */
875
+ current?: FindMatch;
876
+ };
877
+ /** Server-side find and replace configuration. */
878
+ type FindAndReplaceConfig = {
879
+ /** Called when the user types a search query. Returns total count and first match. */
880
+ onFind: (params: FindParams) => Promise<FindResponse>;
881
+ /** Called when the user clicks prev/next arrows. Returns the target match. */
882
+ onNavigate: (params: FindNavigateParams) => Promise<FindMatch>;
883
+ /** Called when the user clicks Replace or Replace All. */
884
+ onReplace: (params: ReplaceParams) => Promise<ReplaceResponse>;
885
+ };
886
+ type StoreMode = "client" | "server";
887
+ type ServerRowId = string | number;
888
+ /** Row-level status flags returned by the server. Drive row filters and sidebar counts. */
889
+ type ServerRowStatus = {
890
+ edited?: boolean;
891
+ new?: boolean;
892
+ deleted?: boolean;
893
+ hasErrors?: boolean;
894
+ hasEmptyCells?: boolean;
895
+ };
896
+ /** Server-reported change for a single cell. The current value lives in `fields`. */
897
+ type ServerCellChange = {
898
+ original: unknown;
899
+ };
900
+ /** Server-reported validation error for a single cell. */
901
+ type ServerCellError = {
902
+ message: string;
903
+ code?: number | string;
904
+ };
905
+ /** Per-row metadata returned by the server in server-delegated mode. */
906
+ type ServerRowMeta = {
907
+ /** Row-level status flags — drive row filters and sidebar counts. */
908
+ status?: ServerRowStatus;
909
+ /** Cell-level change tracking. Key = field name. */
910
+ changes?: Record<string, ServerCellChange>;
911
+ /** Cell-level validation errors. Key = field name. */
912
+ errors?: Record<string, ServerCellError[]>;
913
+ };
914
+ /** Per-source row count returned by the server. Drives the Data Sources sidebar. */
915
+ type ServerSourceCount = {
916
+ id: string;
917
+ name: string;
918
+ count: number;
919
+ };
920
+ /** Aggregate row counts returned alongside a query page. Drive sidebar indicators. */
921
+ type ServerQueryCounts = {
922
+ edited?: number;
923
+ new?: number;
924
+ deleted?: number;
925
+ errors?: number;
926
+ emptyCells?: number;
927
+ sources?: ServerSourceCount[];
928
+ };
929
+ type ServerRow<T extends DataEditorRow = DataEditorRow> = {
930
+ id: ServerRowId;
931
+ fields: T;
932
+ meta?: ServerRowMeta;
933
+ };
934
+ type ServerResponse<T> = {
935
+ data: T;
936
+ meta?: Record<string, unknown>;
937
+ };
938
+ type QueryFilters<F = Record<string, unknown>> = Partial<Filters> & F;
939
+ type QueryParams<F = Record<string, unknown>> = {
940
+ filters?: QueryFilters<F>;
941
+ sort?: SortState;
942
+ /** When present, only rows belonging to these source IDs are returned. Omit to include all sources. */
943
+ sources?: string[];
944
+ offset?: number;
945
+ limit: number;
946
+ signal?: AbortSignal;
947
+ };
948
+ type QueryResponse<T extends DataEditorRow = DataEditorRow> = ServerResponse<{
949
+ rows: ServerRow<T>[];
950
+ totalCount: number;
951
+ filteredCount?: number;
952
+ counts?: ServerQueryCounts;
953
+ }>;
864
954
  /**
865
- * DirtyTracker Change classification and revert detection for rows.
955
+ * Filter options returned by `onFilterOptions` for populating sidebar filter controls in server mode.
956
+ * Keys are column IDs. Only include columns that have a `filter` configured.
957
+ */
958
+ type FilterOptionsResponse = {
959
+ [columnId: string]: {
960
+ /** Values for `"select"` filters. Raw display strings — no formatter is applied. */
961
+ options?: string[];
962
+ /** Bounds for `"number-range"` filters. */
963
+ range?: {
964
+ min: number;
965
+ max: number;
966
+ };
967
+ /** Bounds for `"date-range"` filters. Values are ISO date strings (YYYY-MM-DD). */
968
+ dateRange?: {
969
+ min: string;
970
+ max: string;
971
+ };
972
+ };
973
+ };
974
+ type ServerCallOptions = {
975
+ signal: AbortSignal;
976
+ };
977
+ /**
978
+ * Coordinate rectangle within the server's data view.
979
+ * Uses ServerRowId (primary key) for rows and column ID strings for columns.
866
980
  *
867
- * Every row falls into one of three categories:
868
- * - Clean — matches the backend; not tracked here at all
869
- * - New — created locally (CSV import or manual add), never existed on the backend
870
- * - Edited — exists on the backend but has been modified locally
981
+ * Convention:
982
+ * - Both row fields omitted all rows.
983
+ * - Both column fields omitted all columns.
984
+ * - Single row: `fromRow` AND `toRow` both set, `toRow === fromRow`.
985
+ * - Single column: `fromColumn` AND `toColumn` both set, `toColumn === fromColumn`.
986
+ * - Range: both `from` and `to` set, `to !== from`.
987
+ * - `allSelected: true` → all rows and all columns.
988
+ * - Empty `{}` → used only for insert operations.
871
989
  *
872
- * Classification uses inverted tracking via `nonBackendRowIds`: only non-backend
873
- * rows (CSV imports, manual adds) are stored in the set. Since backend rows are
874
- * the vast majority (~1M), this inverted approach saves ~50MB by assuming any
875
- * row NOT in the set is a backend row.
876
- * - First edit of a backend row → "edited", original row is snapshotted
877
- * - First edit of a non-backend row → "new"
990
+ * INVALID: `from` present without `to`, or vice versa.
991
+ */
992
+ type Region = {
993
+ fromRow?: ServerRowId;
994
+ toRow?: ServerRowId;
995
+ fromColumn?: string;
996
+ toColumn?: string;
997
+ allSelected?: boolean;
998
+ };
999
+ /** Describes where and how to insert a new row. */
1000
+ type InsertParams = {
1001
+ /** Existing row to anchor the insert relative to. Omitted when appending to the end. */
1002
+ anchorRow?: ServerRowId;
1003
+ /** Insert before or after the anchor row. */
1004
+ position: "above" | "below";
1005
+ /** Column IDs matching the order of `values` entries. */
1006
+ columns: string[];
1007
+ };
1008
+ /**
1009
+ * Unified edit params for all data mutations in server-delegated mode.
878
1010
  *
879
- * Smart revert: after every edit, the current row is compared field-by-field
880
- * against the snapshot. If all fields match the original, the row silently
881
- * reverts to "clean" no dirty flag, no undo entry needed. This lets users
882
- * fix typos by simply typing the original value back.
1011
+ * The combination of fields determines the operation:
1012
+ * - `target` + `values` → cell edit, clear, or external paste
1013
+ * - `target` + `source` → internal paste (+ `cut` for cut-paste)
1014
+ * - `target` + `source` (fill) → fill handle
1015
+ * - `target` + `transform` → transform / revert
1016
+ * - `target` + `delete: true` → mark rows for deletion
1017
+ * - `target` + `delete: false` → restore rows (unmark deletion)
1018
+ * - `insert` + `values` → create a new row
883
1019
  *
884
- * Merge snapshots: when a CSV import overwrites an existing row, the original
885
- * data + classification is saved so it can be restored if the source is removed.
1020
+ * `values` is always a 2D array. For a single cell edit: `[["newValue"]]`.
1021
+ * For clearing: `[[""]]`. The server interprets dimensions relative to `target`:
1022
+ * a 1×1 `values` applied to a multi-cell target means "fill all cells with this value".
1023
+ *
1024
+ * `filters` and `sort` provide the view context so the server can resolve
1025
+ * which rows fall between `fromRow` and `toRow` in the current view.
1026
+ * Omitted for single-cell edits where `fromRow` is a direct row ID.
886
1027
  */
887
-
888
- type IDirtyTracker<TRow extends DataEditorRow = DataEditorRow> = {
889
- markDeleted(id: TRowId): void;
890
- unmarkDeleted(id: TRowId): void;
891
- isDeleted(id: TRowId): boolean;
892
- getDeletedRowIds(): ReadonlySet<TRowId>;
893
- deletedCount(): number;
894
- isDefaultSourceRow(id: TRowId): boolean;
895
- isNew(id: TRowId): boolean;
896
- isEdited(id: TRowId): boolean;
897
- isCellDirty(id: TRowId, field: string, currentRow: TRow | undefined): boolean;
898
- getOriginalCellValue(id: TRowId, field: string): unknown | undefined;
899
- getOriginalRow(id: TRowId): TRow | undefined;
900
- hasOriginalRow(id: TRowId): boolean;
901
- trackNonBackendRow(id: TRowId): void;
902
- markNew(id: TRowId): void;
903
- markEdited(id: TRowId): void;
904
- snapshotOriginal(id: TRowId, row: TRow): void;
905
- classifyOnFirstEdit(id: TRowId, row: TRow): void;
906
- checkRevert(id: TRowId, currentRow: TRow): {
907
- wasNew: boolean;
908
- wasEdited: boolean;
909
- };
910
- removeTracking(id: TRowId): void;
911
- getNewRowIds(): ReadonlySet<TRowId>;
912
- getEditedRowIds(): ReadonlySet<TRowId>;
913
- getMergeSnapshot(id: TRowId): {
914
- isNew: boolean;
915
- isEdited: boolean;
916
- };
917
- restoreMergeClassification(id: TRowId, isNew: boolean, isEdited: boolean): void;
918
- clear(): void;
1028
+ type EditParams = {
1029
+ target: Region[];
1030
+ source?: Region[];
1031
+ values?: unknown[][];
1032
+ transform?: TransformParams;
1033
+ cut?: boolean;
1034
+ delete?: boolean;
1035
+ insert?: InsertParams;
1036
+ undo?: boolean;
1037
+ filters?: QueryFilters;
1038
+ sort?: SortState;
1039
+ lockedColumns?: Array<{
1040
+ columnId: string;
1041
+ mode: "all" | "default";
1042
+ }>;
919
1043
  };
920
-
921
- interface FlagReader {
922
- hasError(id: TRowId): boolean;
923
- isNew(id: TRowId): boolean;
924
- isEdited(id: TRowId): boolean;
925
- hasEmptyCells(id: TRowId): boolean;
926
- isDeleted(id: TRowId): boolean;
927
- hasDeletedRows(): boolean;
928
- getSourceId(id: TRowId): string;
929
- getErrorBitmask(id: TRowId, fieldOrder: string[], wordIdx: number): number;
930
- getEditedBitmask(id: TRowId, fieldOrder: string[], wordIdx: number): number;
931
- getErrorMessageToRows(): ReadonlyMap<string, ReadonlySet<TRowId>>;
932
- getRowValidations(id: TRowId): Map<string, NonNullable<ValidationResult>> | undefined;
933
- }
934
- interface FilterRowReader<TRow extends DataEditorRow = DataEditorRow> {
935
- getRowIds(): TRowId[];
936
- getRowById(id: TRowId): TRow | undefined;
937
- getHiddenSourceIds(): Set<DataSourceId>;
938
- }
939
- type IFilterEngine<TRow extends DataEditorRow = DataEditorRow> = {
940
- getFilteredRowIds(): TRowId[] | null;
941
- getBaseFilteredRowIds(): TRowId[] | null;
942
- getFilterVersion(): number;
943
- getFilterCriteriaVersion(): number;
944
- isFiltering(): boolean;
945
- getFilters(): Filters;
946
- getFieldOrder(): string[];
947
- getWordsPerRow(): number;
948
- getShowOnlyDeletedRows(): boolean;
949
- getSortState(): SortState;
950
- setReaders(rowReader: FilterRowReader<TRow>, flagReader: FlagReader): void;
951
- setColumns(columns: DataEditorColumn[]): void;
952
- setFilters(filters: Partial<Filters>): void;
953
- setSortState(state: SortState, sortType?: SortType, locales?: string[]): Promise<void>;
954
- updateRowText(rowId: TRowId, row: TRow): void;
955
- updateRowsText(rows: {
956
- id: TRowId;
957
- row: TRow;
958
- }[]): void;
959
- deleteRowTextCache(rowId: TRowId): void;
960
- clearRowTextCacheAll(): void;
961
- setRowTextCache(rowId: TRowId, row: TRow): void;
962
- rebuild(): void;
963
- refilterAfterColumnsChange(): void;
964
- notifyRowsAdded(ids: TRowId[]): void;
965
- notifyRowsDeleted(deletedIds: TRowId[]): void;
966
- notifyRowsInserted(restoredIds: TRowId[], positions: number[]): void;
967
- refilterAfterSourceToggle(sourceId: DataSourceId, isVisible: boolean): void;
968
- refilterAfterFlagChange(): void;
969
- flushPendingFlags(): void;
970
- markFlagsDirty(): void;
971
- testRowAgainstFilters(rowId: TRowId, row: TRow, flagReader: FlagReader, hiddenSourceIds: Set<DataSourceId>): boolean;
972
- getAllRowIdsSorted(rowReader: FilterRowReader<TRow>): Promise<TRowId[]>;
973
- clear(): void;
974
- destroy(): void;
1044
+ type ExportParams$1<F = Record<string, unknown>> = {
1045
+ format: "csv" | "tsv" | "xlsx" | "json" | "xml";
1046
+ allRows: boolean;
1047
+ rtl?: boolean;
1048
+ filters?: QueryFilters<F>;
1049
+ sort?: SortState;
1050
+ signal?: AbortSignal;
1051
+ };
1052
+ /** Transform operation descriptor. `type` identifies the operation, optional fields carry parameters. */
1053
+ type TransformParams = {
1054
+ type: string;
1055
+ separator?: string;
1056
+ /** When `true`, the server should delete the dynamic source columns after applying the transform. */
1057
+ deleteSource?: boolean;
1058
+ };
1059
+ /** Params passed to `onColumnDelete` when the user deletes a dynamic column. */
1060
+ type ColumnDeleteParams = {
1061
+ /** ID of the dynamic column to delete. */
1062
+ columnId: string;
1063
+ /** `true` when the server should reverse this operation (undo). Omitted on initial call and redo. */
1064
+ undo?: boolean;
1065
+ };
1066
+ /** Params passed to `onColumnEdit` when the user renames a dynamic column. */
1067
+ type ColumnEditParams = {
1068
+ /** ID of the dynamic column being edited. */
1069
+ columnId: string;
1070
+ /** New title for the column. */
1071
+ title: string;
1072
+ /** `true` when the server should reverse this operation (undo). Omitted on initial call and redo. */
1073
+ undo?: boolean;
1074
+ };
1075
+ /** Server's response after applying a mutation. */
1076
+ type EditResponse = {
1077
+ counts?: ServerQueryCounts;
1078
+ /** Business-logic rejection. SDK reverts the optimistic update and shows `reason` in a toast. */
1079
+ rejected?: boolean;
1080
+ /** Why the server rejected. Shown to the user as-is. */
1081
+ reason?: string;
1082
+ /** The full row created by the server. Returned for insert operations. */
1083
+ row?: ServerRow;
1084
+ /** Updated column list. When present, the SDK replaces its columns with this list. */
1085
+ columns?: Array<{
1086
+ id: string;
1087
+ title: string;
1088
+ }>;
1089
+ };
1090
+ /**
1091
+ * Every decision the user made during the import wizard, packed into one object.
1092
+ * You get the raw file and the full mapping config. Parse it however you want —
1093
+ * stream it, bulk-load it, hand it to a background job. Your call.
1094
+ */
1095
+ type ImportMappings = {
1096
+ /** CSV header → column ID. Headers the user left unmatched are `undefined`. */
1097
+ columnMapping: Record<string, string | undefined>;
1098
+ /**
1099
+ * Value substitutions for select columns.
1100
+ * Outer key = column ID, inner key = imported value, inner value = target option.
1101
+ * Only present when at least one select column was matched.
1102
+ */
1103
+ valueMapping: Record<string, Record<string, string | undefined>>;
1104
+ /** Column ID used to match imported rows against existing data. Same value as `DataEditorProps.primaryKey`. */
1105
+ primaryKey: string;
1106
+ /** Sheet name the user selected. Only present for multi-sheet XLSX files. */
1107
+ selectedSheet?: string;
1108
+ /** Zero-based index of the row the SDK detected as the header row. */
1109
+ headerRowIndex: number;
1110
+ /** Number format detected from the file contents. Affects how `"1.234,56"` vs `"1,234.56"` is read. */
1111
+ numberFormat: "EU" | "US";
1112
+ /**
1113
+ * Per-column date disambiguation. Key = CSV header (not column ID).
1114
+ * `"EU"` = day-first, `"US"` = month-first.
1115
+ * Only includes columns where the format was ambiguous and had to be resolved.
1116
+ */
1117
+ dateFormats: Record<string, "EU" | "US">;
1118
+ /**
1119
+ * Columns the user created during import for unmatched headers.
1120
+ * These don't exist in your schema yet — you decide whether to persist them.
1121
+ */
1122
+ newColumns: DataEditorColumn[];
1123
+ };
1124
+ /** Params passed to `onFileImport`. The original file, unchanged, plus everything the wizard collected. */
1125
+ type FileImportParams = {
1126
+ /** The original file. Same bytes the user dropped into the browser. */
1127
+ file: File;
1128
+ /** All mapping decisions from the import wizard. */
1129
+ mappings: ImportMappings;
1130
+ /** Display name for this import source. Typically the file name. */
1131
+ sourceName: string;
1132
+ /** Fires when the user cancels the import. */
1133
+ signal?: AbortSignal;
1134
+ };
1135
+ /**
1136
+ * Params passed to `onRowsImport` once per chunk.
1137
+ * The SDK already parsed the file, applied column mappings, normalized dates
1138
+ * and numbers, and resolved value mappings. You get clean, schema-conformant rows.
1139
+ */
1140
+ type RowsImportParams = {
1141
+ /** Stable ID for this import session. Use it for idempotency or correlation. */
1142
+ importId: string;
1143
+ /** Display name for this import source. Typically the file name. */
1144
+ sourceName: string;
1145
+ /** Zero-based chunk index. Together with `importId`, uniquely identifies each chunk. */
1146
+ chunkIndex: number;
1147
+ /** `true` on the last chunk. Safe to finalize, run post-import hooks, or trigger validation. */
1148
+ isLastChunk: boolean;
1149
+ /** Transformed rows, keyed by column ID. Ready to store. */
1150
+ rows: Record<string, unknown>[];
1151
+ /** Columns the user created during import. Only present on the first chunk (`chunkIndex === 0`). */
1152
+ newColumns?: DataEditorColumn[];
1153
+ /** Column ID used to match imported rows against existing data. */
1154
+ primaryKey: string;
1155
+ /** Fires when the user cancels mid-import. Clean up any partial state. */
1156
+ signal?: AbortSignal;
1157
+ };
1158
+ /** Your response after processing a chunk. */
1159
+ type RowsImportResponse = {
1160
+ /** How many rows you accepted from this chunk. Drives progress reporting. */
1161
+ accepted: number;
1162
+ /** Per-row errors. `rowIndex` is relative to the chunk (0-based). Surfaced in the UI after import. */
1163
+ errors?: Array<{
1164
+ rowIndex: number;
1165
+ message: string;
1166
+ }>;
1167
+ };
1168
+ /** Params passed to `onSourceRemove` when the user deletes a data source. */
1169
+ type SourceRemoveParams = {
1170
+ /** The source ID to remove. Matches the `id` from `ServerQueryCounts.sources`. */
1171
+ sourceId: string;
1172
+ /** Fires when the user cancels. */
1173
+ signal?: AbortSignal;
975
1174
  };
976
1175
 
977
1176
  /**
978
- * RowStoreRow storage with dual-access pattern for the grid engine.
1177
+ * ChunkedProcessorGeneric utility for processing items in prioritized chunks
1178
+ * with cancellation support via generation counter.
979
1179
  *
980
- * Maintains two parallel structures:
981
- * - (TRow | undefined)[] → O(1) lookup by stable internal ID (rows[id])
982
- * - TRowId[] → O(1) lookup by visual index (required by the grid)
1180
+ * Used to break O(N) synchronous operations into non-blocking background work:
1181
+ * 1. Priority items are processed synchronously first (e.g. viewport rows)
1182
+ * 2. Remaining items are processed in chunks via MessageChannel
1183
+ * 3. Between chunks the browser can paint frames and handle events
983
1184
  *
984
- * The grid calls getRow(index) on every frame, so index-based access must be
985
- * instant. Filtered views are supported by passing an alternate ID array
986
- * (filteredIds) to the access methods — the store itself is unaware of filters.
1185
+ * Uses MessageChannel instead of setTimeout(0) to avoid the browser's ~4ms
1186
+ * minimum delay imposed on nested setTimeouts, reducing scheduling overhead
1187
+ * from ~6ms/chunk to ~1ms/chunk.
987
1188
  *
988
- * An inverted index (rowId visual index) is lazily rebuilt on demand for
989
- * O(1) reverse lookups (e.g., scrolling to a specific row after undo).
1189
+ * Cancellation uses the same generation counter pattern as FilterEngine:
1190
+ * calling cancel() or run() again increments the generation, and any
1191
+ * in-flight MessageChannel callbacks silently exit when they detect
1192
+ * their generation is stale.
990
1193
  *
991
- * Internal row IDs are auto-generated as sequential numbers (1, 2, 3, …).
992
- * Using a plain array indexed by ID eliminates Map hash table overhead
993
- * (~10MB saved at 1M rows). Deleted slots are set to undefined (not delete)
994
- * to keep V8 in HOLEY_ELEMENTS mode.
1194
+ * This class has zero domain knowledge it works with any item type.
995
1195
  */
1196
+ type ChunkedProcessorCallbacks<T> = {
1197
+ /** All items to process (excluding priority items). */
1198
+ all: T[];
1199
+ /** Items processed synchronously before any chunking begins. */
1200
+ priority: T[];
1201
+ /** Called for each batch of items (priority batch first, then chunks). */
1202
+ onChunk: (items: T[]) => void;
1203
+ /** Called after each chunk with (processed so far, total). */
1204
+ onProgress: (processed: number, total: number) => void;
1205
+ /** Called once when all items have been processed. */
1206
+ onComplete: () => void;
1207
+ /** Called when processing is cancelled via cancel(). */
1208
+ onCancel: () => void;
1209
+ };
1210
+ declare class ChunkedProcessor<T> {
1211
+ private _generation;
1212
+ private _isRunning;
1213
+ private _chunkSize;
1214
+ private _activeOnCancel;
1215
+ private _channel;
1216
+ private _pendingChunkFn;
1217
+ constructor(chunkSize?: number);
1218
+ get isRunning(): boolean;
1219
+ get chunkSize(): number;
1220
+ run(params: ChunkedProcessorCallbacks<T>): void;
1221
+ cancel(): void;
1222
+ }
996
1223
 
997
- declare class RowStore<TRow extends DataEditorRow = DataEditorRow> {
998
- private rows;
999
- private rowIds;
1000
- private rowIdCounter;
1001
- private rowIdToIndex;
1002
- private _rowIndexDirty;
1003
- nextRowId(): TRowId;
1004
- getRow(index: number, filteredIds: TRowId[] | null): TRow | undefined;
1005
- getRowById(id: TRowId): TRow | undefined;
1006
- getRowId(index: number, filteredIds: TRowId[] | null): TRowId | undefined;
1007
- getRowIndex(rowId: TRowId, filteredIds: TRowId[] | null): number;
1008
- getRowIds(): TRowId[];
1009
- getRowCount(): number;
1010
- allRows(): IterableIterator<TRow>;
1011
- hasRow(id: TRowId): boolean;
1012
- setRow(id: TRowId, row: TRow): void;
1013
- deleteRow(id: TRowId): void;
1014
- pushRowId(id: TRowId): void;
1015
- spliceRowId(pos: number, id: TRowId): void;
1016
- removeRowId(id: TRowId): void;
1017
- filterRowIds(predicate: (id: TRowId) => boolean): void;
1018
- trimFromStart(count: number): TRowId[];
1019
- trimFromEnd(count: number): TRowId[];
1020
- unshiftRowIds(ids: TRowId[]): void;
1021
- invalidateIndex(): void;
1224
+ type RegisterSourceOptions = {
1225
+ name: string;
1226
+ id?: DataSourceId;
1227
+ isDeletable?: boolean;
1228
+ isInitialData?: boolean;
1229
+ };
1230
+ type MergeEntry<TRow> = {
1231
+ row: TRow;
1232
+ sourceId: DataSourceId;
1233
+ isNew: boolean;
1234
+ isEdited: boolean;
1235
+ };
1236
+ type RemovalPlan<TRow> = {
1237
+ rowsToDelete: Set<TRowId>;
1238
+ rowsToRestore: Array<{
1239
+ rowId: TRowId;
1240
+ row: TRow;
1241
+ originalSourceId: DataSourceId;
1242
+ isNew: boolean;
1243
+ isEdited: boolean;
1244
+ }>;
1245
+ };
1246
+ type ExtendedRemovalPlan<TRow> = RemovalPlan<TRow> & {
1247
+ sourceId: DataSourceId;
1248
+ repairedEntries: Array<{
1249
+ sourceId: DataSourceId;
1250
+ rowId: TRowId;
1251
+ before: MergeEntry<TRow>;
1252
+ after: MergeEntry<TRow> | null;
1253
+ }>;
1254
+ };
1255
+ declare class SourceManager<TRow extends DataEditorRow = DataEditorRow> {
1256
+ private readonly _defaultSourceId;
1257
+ private readonly overrides;
1258
+ private readonly sources;
1259
+ private readonly mergedRows;
1260
+ getSourceId(rowId: TRowId): DataSourceId;
1261
+ setSourceId(rowId: TRowId, sourceId: DataSourceId): void;
1262
+ deleteSourceId(rowId: TRowId): void;
1263
+ getOverrides(): ReadonlyMap<TRowId, DataSourceId>;
1264
+ register(options: RegisterSourceOptions): DataSourceId;
1265
+ /**
1266
+ * Re-insert a source using a full captured state, preserving
1267
+ * isVisible, isLoading, rowCount, etc. Used by SourceLifecycle.restore.
1268
+ * If the source already exists, overwrites its state.
1269
+ */
1270
+ restoreState(state: DataSourceState): void;
1271
+ has(sourceId: DataSourceId): boolean;
1272
+ get(sourceId: DataSourceId): DataSourceState | undefined;
1273
+ delete(sourceId: DataSourceId): void;
1274
+ setLoading(sourceId: DataSourceId, isLoading: boolean): void;
1275
+ finalizeAllSources(): void;
1276
+ values(): IterableIterator<DataSourceState>;
1277
+ getHiddenSourceIds(): Set<DataSourceId>;
1278
+ saveMergeSnapshot(sourceId: DataSourceId, rowId: TRowId, existingRow: TRow, previousSourceId: DataSourceId, isNew: boolean, isEdited: boolean): void;
1279
+ /**
1280
+ * Public so commands can re-install merge entries during undo of a remove.
1281
+ */
1282
+ restoreMergeEntry(sourceId: DataSourceId, rowId: TRowId, entry: MergeEntry<TRow>): void;
1283
+ /**
1284
+ * Pure — computes the full removal plan without mutating any state.
1285
+ * Callers run applyRemovalPlan(plan) to commit.
1286
+ */
1287
+ planRemoval(sourceId: DataSourceId): ExtendedRemovalPlan<TRow> | null;
1288
+ /**
1289
+ * Mutates internal state per plan produced by planRemoval.
1290
+ */
1291
+ applyRemovalPlan(plan: ExtendedRemovalPlan<TRow>): void;
1022
1292
  clear(): void;
1023
- private rebuildRowIdToIndex;
1293
+ private getUniqueName;
1024
1294
  }
1025
1295
 
1026
1296
  /**
1027
- * SnapshotManager Immutable snapshot construction and listener notification
1028
- * for React's useSyncExternalStore integration.
1029
- *
1030
- * Builds a DataStoreSnapshot object that React components subscribe to via
1031
- * useSyncExternalStore(subscribe, getSnapshot). A new snapshot object is
1032
- * created on every notify() call, which triggers React's shallow comparison
1033
- * and re-renders only when values actually change.
1297
+ * Internal contract: the surface a Scale client exposes to the SDK's data
1298
+ * layer. Implemented by `ScaleClient`. Not part of the public SDK API —
1299
+ * customers configure server mode via `server: { url }` and never see this
1300
+ * type.
1301
+ */
1302
+ type ScaleClientApi<TRow extends DataEditorRow = DataEditorRow, TFilters = Record<string, unknown>> = {
1303
+ onQuery: (params: QueryParams<TFilters>) => Promise<QueryResponse<TRow>>;
1304
+ onFilterOptions?: () => Promise<FilterOptionsResponse>;
1305
+ onExport?: (params: ExportParams$1<TFilters>) => Promise<void>;
1306
+ onEdit: (params: EditParams, options?: ServerCallOptions) => Promise<EditResponse | void>;
1307
+ onFileImport?: (params: FileImportParams) => Promise<void>;
1308
+ onRowsImport?: (params: RowsImportParams) => Promise<RowsImportResponse | void>;
1309
+ importChunkSize?: number;
1310
+ onSourceRemove?: (params: SourceRemoveParams) => Promise<void>;
1311
+ onColumnDelete?: (params: ColumnDeleteParams) => Promise<EditResponse | void>;
1312
+ onColumnEdit?: (params: ColumnEditParams) => Promise<EditResponse | void>;
1313
+ findAndReplace?: FindAndReplaceConfig;
1314
+ pageSize?: number;
1315
+ scrollSensitivity?: number;
1316
+ };
1317
+
1318
+ type ServerDataManagerDeps<TRow extends DataEditorRow = DataEditorRow> = {
1319
+ clear(): void;
1320
+ setLoading(isLoading: boolean): void;
1321
+ registerSource(options: RegisterSourceOptions): DataSourceId;
1322
+ setSourceLoading(sourceId: DataSourceId, isLoading: boolean): void;
1323
+ getLocalRowCount(): number;
1324
+ replaceServerRows(sourceId: DataSourceId, rows: ServerRow<TRow>[], offset: number, counts?: ServerQueryCounts): void;
1325
+ appendServerRows(rows: ServerRow<TRow>[], counts?: ServerQueryCounts): void;
1326
+ prependServerRows(rows: ServerRow<TRow>[], offset: number, counts?: ServerQueryCounts): void;
1327
+ applyServerRowMeta(rows: ServerRow<TRow>[], counts?: ServerQueryCounts): void;
1328
+ };
1329
+ type FetchDirection = "forward" | "backward" | "jump";
1330
+ declare class ServerDataManager<TRow extends DataEditorRow = DataEditorRow> {
1331
+ private _offset;
1332
+ private _totalCount;
1333
+ private _isFetching;
1334
+ private readonly _pageSize;
1335
+ private readonly _maxBufferRows;
1336
+ private _filters;
1337
+ private _sources;
1338
+ private _sort;
1339
+ private _filterOptions;
1340
+ private _filterOptionsFetched;
1341
+ private _abortController;
1342
+ private _syncAbort;
1343
+ private _lastVisibleStart;
1344
+ private _debouncedFetch;
1345
+ private readonly _config;
1346
+ private readonly _dataStoreRef;
1347
+ private readonly _sourceLabel;
1348
+ private _onChanged;
1349
+ constructor(config: ScaleClientApi<TRow>, dataStoreRef: ServerDataManagerDeps<TRow>, sourceLabel: string);
1350
+ get offset(): number;
1351
+ get totalCount(): number | null;
1352
+ get isFetching(): boolean;
1353
+ get pageSize(): number;
1354
+ get maxBufferRows(): number;
1355
+ setOnChanged(callback: () => void): void;
1356
+ setOffset(offset: number): void;
1357
+ setTotalCount(count: number): void;
1358
+ private setFetching;
1359
+ getExcess(currentCount: number, newCount: number): number;
1360
+ shouldFetch(visibleStart: number, visibleEnd: number, loadedCount: number): FetchDirection | null;
1361
+ /**
1362
+ * Full reload — abort in-flight, clear store, fetch first page.
1363
+ * Called on initial load, search/filter/sort changes, and resetFilters.
1364
+ */
1365
+ reload(): void;
1366
+ /**
1367
+ * Scroll-driven pagination with velocity-based debouncing.
1368
+ * Called from CanvasGrid on every scroll event.
1369
+ */
1370
+ handleScroll(visibleStart: number, visibleEnd: number): void;
1371
+ /**
1372
+ * Fetch a single page based on scroll position and current window state.
1373
+ */
1374
+ private fetchPage;
1375
+ /**
1376
+ * Re-query the current viewport and replace row data, metadata, and counts.
1377
+ * Called after successful edits and find-and-replace mutations.
1378
+ * Each call aborts the previous in-flight sync.
1379
+ */
1380
+ syncCurrentView(): void;
1381
+ /**
1382
+ * Merge filter keys into server filter state and reload.
1383
+ * Called by DataStore.setFilters() in server mode and by filter components.
1384
+ */
1385
+ setFilters(filters: Partial<Filters>): void;
1386
+ /**
1387
+ * Set sort state and reload.
1388
+ */
1389
+ setSort(sort: SortState): void;
1390
+ /**
1391
+ * Restrict query to visible sources and reload.
1392
+ * Pass `undefined` to include all sources.
1393
+ */
1394
+ setSources(sources: string[] | undefined): void;
1395
+ /**
1396
+ * Clear all filters and sort, then reload.
1397
+ */
1398
+ resetFilters(): void;
1399
+ /**
1400
+ * One-time fetch of filter option dictionaries for sidebar filter controls.
1401
+ */
1402
+ fetchFilterOptions(): void;
1403
+ getFilterOptions(): FilterOptionsResponse | null;
1404
+ get onEdit(): (params: EditParams, options?: ServerCallOptions) => Promise<EditResponse | void>;
1405
+ get filters(): Record<string, unknown>;
1406
+ get sort(): SortState;
1407
+ get sources(): string[] | undefined;
1408
+ get onSourceRemove(): ((params: SourceRemoveParams) => Promise<void>) | undefined;
1409
+ get onColumnDelete(): ((params: ColumnDeleteParams) => Promise<EditResponse | void>) | undefined;
1410
+ get onColumnEdit(): ((params: ColumnEditParams) => Promise<EditResponse | void>) | undefined;
1411
+ get hasExport(): boolean;
1412
+ private _exportAbortController;
1413
+ export(format: DataEditorFormat, allRows: boolean, rtl: boolean): Promise<void>;
1414
+ clear(): void;
1415
+ destroy(): void;
1416
+ }
1417
+
1418
+ /**
1419
+ * A post-resolution selection rectangle in stable coordinate space.
1420
+ * Produced by grid-layer resolvers from CellRange[] in grid-index space.
1421
+ * Consumed by DataStore operations and server sync.
1422
+ */
1423
+ type SelectionRect = {
1424
+ readonly fields: readonly string[];
1425
+ readonly rowIds: readonly TRowId[];
1426
+ };
1427
+
1428
+ /**
1429
+ * ServerEditBuilder — Stateless coordinate translator for server-delegated edits.
1034
1430
  *
1035
- * Count tracking has two tiers:
1036
- * - Visible counts: new/edited/error/empty rows from visible sources only.
1037
- * Updated incrementally via adjust*() methods on single-row edits, or
1038
- * recomputed in bulk when _countsDirty is set (after deletes, source changes, etc.).
1039
- * - Filtered counts: subset of visible counts restricted to filteredRowIds.
1040
- * Recomputed when _filteredCountsDirty is set (after filter changes).
1431
+ * Converts frontend coordinates (TRowId, column index, grid ranges) into
1432
+ * `EditParams` with `Region[]` that the server can interpret.
1041
1433
  *
1042
- * Batching: multiple operations can be grouped via beginBatch()/endBatch().
1043
- * While batching, notify() calls are deferred only the outermost endBatch()
1044
- * triggers a single notification, preventing intermediate snapshots from
1045
- * reaching React during multi-step operations (fill handle, batch delete, etc.).
1434
+ * Does NOT call `onEdit`. Only builds params.
1435
+ * DataStore calls the builder, then sends the result to the server.
1046
1436
  *
1047
- * The SnapshotStateReader interface decouples this module from all other modules.
1048
- * DataStore implements the interface by delegating to RowStore, DirtyTracker,
1049
- * FilterEngine, and ValidationStore, so SnapshotManager never imports them directly.
1437
+ * Responsibilities:
1438
+ * - TRowId ServerRowId translation via primaryKey
1439
+ * - Grid index column ID translation via columns array
1440
+ * - Filter/sort context attachment from ServerDataManager
1050
1441
  */
1051
1442
 
1052
- type Listener = () => void;
1053
- interface SnapshotStateReader {
1054
- getRowCount(): number;
1055
- getFilteredRowIds(): TRowId[] | null;
1056
- getBaseFilteredRowIds(): TRowId[] | null;
1057
- getNewRowIds(): ReadonlySet<TRowId>;
1058
- getEditedRowIds(): ReadonlySet<TRowId>;
1059
- getRowsWithErrors(): ReadonlySet<TRowId>;
1060
- getRowsWithEmptyCells(): ReadonlySet<TRowId>;
1061
- getDeletedCount(): number;
1062
- isDeleted(id: TRowId): boolean;
1063
- getSources(): DataSourceState[];
1064
- isRowVisible(id: TRowId): boolean;
1065
- canUndo(): boolean;
1066
- canRedo(): boolean;
1067
- isLoading(): boolean;
1068
- isFiltering(): boolean;
1069
- getVersion(): number;
1070
- getFilterCriteriaVersion(): number;
1071
- getFilteredNewCount(ids: TRowId[] | null): number;
1072
- getFilteredEditedCount(ids: TRowId[] | null): number;
1073
- getFilteredDirtyCount(ids: TRowId[] | null): number;
1074
- getFilteredErrorCount(ids: TRowId[] | null): number;
1075
- getFilteredEmptyCount(ids: TRowId[] | null): number;
1076
- getErrorMessageCounts(filteredRowIds: TRowId[] | null): Record<string, number>;
1077
- hasColumnScoping(): boolean;
1078
- getSortState(): SortState;
1079
- getShowOnlyDeletedRows(): boolean;
1080
- /** Server-provided aggregate counts. Present only in server mode. */
1081
- getServerEditedCount?(): number;
1082
- getServerNewCount?(): number;
1083
- getServerErrorCount?(): number;
1084
- getServerEmptyCount?(): number;
1085
- getServerDeletedCount?(): number;
1443
+ type ServerEditDeps<TRow extends DataEditorRow = DataEditorRow> = {
1444
+ getPrimaryKey: () => string;
1445
+ getRowById: (id: TRowId) => TRow | undefined;
1446
+ getColumnIds: () => string[];
1447
+ getFilters: () => Record<string, unknown>;
1448
+ getSort: () => SortState;
1449
+ getLockedColumns: () => ReadonlyMap<string, ColumnLockMode>;
1450
+ };
1451
+ declare class ServerEditBuilder<TRow extends DataEditorRow = DataEditorRow> {
1452
+ private readonly _deps;
1453
+ constructor(deps: ServerEditDeps<TRow>);
1454
+ resolveServerRowId(rowId: TRowId): ServerRowId | undefined;
1455
+ buildRegion(rowIds: TRowId[], columnIds: string[]): Region;
1456
+ /**
1457
+ * Collapse rowIds × columnIds into minimal Region[].
1458
+ * - All columns → omit column fields (row-only regions).
1459
+ * - Contiguous columns in schema order → single fromColumn/toColumn span.
1460
+ * - Non-contiguous → one region per contiguous column group.
1461
+ * Rows are expressed as fromRow/toRow using first/last of the provided array.
1462
+ */
1463
+ buildRegions(rowIds: TRowId[], columnIds: string[]): Region[];
1464
+ /**
1465
+ * Collapse columnIds into minimal column-only Region[] (all rows implied).
1466
+ * - All columns → `{ allSelected: true }`.
1467
+ * - Contiguous in schema order → single `{ fromColumn, toColumn }`.
1468
+ * - Non-contiguous → one region per contiguous group.
1469
+ */
1470
+ buildColumnRegions(columnIds: string[]): Region[];
1471
+ /**
1472
+ * Build minimal Region[] from multiple selection rectangles.
1473
+ * Each rect is collapsed independently, preserving disjoint selections.
1474
+ */
1475
+ buildRegionsFromRects(rects: SelectionRect[]): Region[];
1476
+ buildAllSelectedRegion(): Region;
1477
+ buildColumnRegion(columnId: string): Region;
1478
+ buildRowRegion(fromRowId: TRowId, toRowId: TRowId): Region;
1479
+ cellEdit(rowId: TRowId, field: string, value: unknown): EditParams;
1480
+ clear(target: Region[]): EditParams;
1481
+ paste(source: Region[], target: Region[], cut?: boolean): EditParams;
1482
+ pasteExternal(target: Region[], values: unknown[][]): EditParams;
1483
+ fill(source: Region[], target: Region[]): EditParams;
1484
+ transform(target: Region[], transform: TransformParams): EditParams;
1485
+ deleteRows(rowRanges: [TRowId, TRowId][]): EditParams;
1486
+ restoreRows(rowRanges: [TRowId, TRowId][]): EditParams;
1487
+ deleteAllRows(): EditParams;
1488
+ restoreAllRows(): EditParams;
1489
+ insertRow(anchorRowId: TRowId | undefined, position: InsertParams["position"], values: unknown[][], columnIds: string[]): EditParams;
1490
+ /**
1491
+ * Returns null when all columns are selected (caller decides representation).
1492
+ * Otherwise returns contiguous column spans as `{ fromColumn, toColumn }` regions.
1493
+ */
1494
+ private collapseColumns;
1495
+ private viewContext;
1086
1496
  }
1087
- declare class SnapshotManager {
1088
- private _visibleNewCount;
1089
- private _visibleEditedCount;
1090
- private _visibleDirtyCount;
1091
- private _visibleErrorCount;
1092
- private _visibleEmptyCount;
1093
- private _countsDirty;
1094
- private _filteredDirtyCount;
1095
- private _filteredNewCount;
1096
- private _filteredEditedCount;
1097
- private _filteredErrorCount;
1098
- private _filteredEmptyCount;
1099
- private _errorMessageCounts;
1100
- private _filteredCountsDirty;
1101
- private _phase;
1102
- private _processingInfo;
1103
- private snapshot;
1104
- private listeners;
1105
- private batchDepth;
1106
- private pendingNotify;
1107
- get countsDirty(): boolean;
1108
- markCountsDirty(): void;
1109
- markFilteredCountsDirty(): void;
1110
- adjustVisibleErrorCount(delta: number): void;
1111
- adjustVisibleNewCount(delta: number): void;
1112
- adjustVisibleEditedCount(delta: number): void;
1113
- adjustVisibleDirtyCount(delta: number): void;
1114
- subscribe: (listener: Listener) => (() => void);
1115
- getSnapshot: () => DataStoreSnapshot;
1116
- isBatching(): boolean;
1117
- beginBatch(): boolean;
1118
- endBatch(): boolean;
1119
- setPhase(phase: ProcessingPhase, info?: ProcessingInfo): void;
1120
- getPhase(): ProcessingPhase;
1121
- notify(reader: SnapshotStateReader): void;
1122
- clear(): void;
1123
- clearListeners(): void;
1124
- private recomputeVisibleCounts;
1125
- private recomputeFilteredCounts;
1497
+
1498
+ /**
1499
+ * Categories of internal errors surfaced through the `onError` callback.
1500
+ *
1501
+ * Existing categories cover client-side failures. `scale.*` codes cover
1502
+ * server-mode (Updog Scale) failures. `license.*` codes cover license
1503
+ * validation failures (previously a separate `LicenseErrorCode` enum).
1504
+ */
1505
+ type UpdogErrorCode = "PARSE_ERROR" | "RENDER_ERROR" | "TRANSFORM_ERROR" | "VALIDATION_ERROR" | "WORKER_ERROR" | "COMMAND_ERROR" | "OPERATION_ERROR" | "license.invalid" | "license.missing" | "license.domain_not_allowed" | "license.subscription_inactive" | "license.trial_expired" | "scale.bootstrap_failed" | "scale.workspace_lost" | "scale.unreachable" | "scale.server_error";
1506
+ /**
1507
+ * An internal error caught by the SDK and passed to `onError`. The SDK
1508
+ * recovers gracefully where possible — `onError` is for your logging and
1509
+ * monitoring (Sentry, Datadog, etc.).
1510
+ *
1511
+ * @example
1512
+ * ```ts
1513
+ * onError={(error) => {
1514
+ * Sentry.captureException(error.originalError ?? error, {
1515
+ * tags: { code: error.code, source: error.source },
1516
+ * });
1517
+ * }}
1518
+ * ```
1519
+ */
1520
+ type UpdogError = {
1521
+ /** The error category. */
1522
+ code: UpdogErrorCode;
1523
+ /** Human-readable description. */
1524
+ message: string;
1525
+ /** Module or subsystem that raised the error. */
1526
+ source: string;
1527
+ /** The underlying thrown value, when available. */
1528
+ originalError?: unknown;
1529
+ };
1530
+
1531
+ declare class ErrorHandler {
1532
+ private onError?;
1533
+ constructor(onError?: (error: UpdogError) => void);
1534
+ handleError(error: UpdogError): void;
1126
1535
  }
1127
1536
 
1128
1537
  /**
1129
- * ValidationStoreCell-level validation state with incremental count tracking.
1538
+ * DirtyTrackerChange classification and revert detection for rows.
1130
1539
  *
1131
- * Stores validation results in a two-level map: rowId → fieldId → ValidationResult[].
1132
- * Maintains a running error count and a pre-computed row-level set
1133
- * (_rowsWithErrors) for O(1) "does this row have errors?" checks.
1540
+ * Every row falls into one of three categories:
1541
+ * - Clean — matches the backend; not tracked here at all
1542
+ * - New — created locally (CSV import or manual add), never existed on the backend
1543
+ * - Edited — exists on the backend but has been modified locally
1134
1544
  *
1135
- * The key design decision is the ValidationDelta return type from setCellValidation().
1136
- * Instead of directly triggering notifications, it returns a delta object describing
1137
- * what changed (error count +/- 1, row-level error membership flipped).
1138
- * The caller (DataStore) uses this delta for incremental snapshot count updates,
1139
- * avoiding a full recount on every validation change.
1545
+ * Classification uses inverted tracking via `nonBackendRowIds`: only non-backend
1546
+ * rows (CSV imports, manual adds) are stored in the set. Since backend rows are
1547
+ * the vast majority (~1M), this inverted approach saves ~50MB by assuming any
1548
+ * row NOT in the set is a backend row.
1549
+ * - First edit of a backend row "edited", original row is snapshotted
1550
+ * - First edit of a non-backend row → "new"
1140
1551
  *
1141
- * Empty cell tracking (_rowsWithEmptyCells) is separate from validation it
1142
- * tracks rows where any visible column has a null/empty value, used by the
1143
- * "show only rows with empty cells" filter.
1552
+ * Smart revert: after every edit, the current row is compared field-by-field
1553
+ * against the snapshot. If all fields match the original, the row silently
1554
+ * reverts to "clean" no dirty flag, no undo entry needed. This lets users
1555
+ * fix typos by simply typing the original value back.
1556
+ *
1557
+ * Merge snapshots: when a CSV import overwrites an existing row, the original
1558
+ * data + classification is saved so it can be restored if the source is removed.
1144
1559
  */
1145
1560
 
1146
- type ValidationDelta = {
1147
- errorDelta: number;
1148
- rowErrorChanged: boolean;
1149
- };
1150
- type IValidationStore = {
1151
- setCellValidation(rowId: TRowId, field: string, result: ValidationResult): ValidationDelta;
1152
- getCellValidation(rowId: TRowId, field: string): ValidationResult;
1153
- clearRowValidations(rowId: TRowId): void;
1154
- hasRowErrors(rowId: TRowId): boolean;
1155
- getRowsWithErrors(): ReadonlySet<TRowId>;
1156
- getRowsWithEmptyCells(): ReadonlySet<TRowId>;
1157
- hasEmptyCells(rowId: TRowId): boolean;
1158
- checkRowEmptyCells(rowId: TRowId, row: Record<string, unknown> | undefined, fieldOrder: string[]): boolean;
1159
- deleteRowTracking(rowId: TRowId): void;
1160
- getErrorCount(): number;
1161
- getRowValidations(rowId: TRowId): Map<string, NonNullable<ValidationResult>> | undefined;
1162
- getErrorMessageToRows(): ReadonlyMap<string, ReadonlySet<TRowId>>;
1561
+ type IDirtyTracker<TRow extends DataEditorRow = DataEditorRow> = {
1562
+ markDeleted(id: TRowId): void;
1563
+ unmarkDeleted(id: TRowId): void;
1564
+ isDeleted(id: TRowId): boolean;
1565
+ getDeletedRowIds(): ReadonlySet<TRowId>;
1566
+ deletedCount(): number;
1567
+ isDefaultSourceRow(id: TRowId): boolean;
1568
+ isNew(id: TRowId): boolean;
1569
+ isEdited(id: TRowId): boolean;
1570
+ isCellDirty(id: TRowId, field: string, currentRow: TRow | undefined): boolean;
1571
+ getOriginalCellValue(id: TRowId, field: string): unknown | undefined;
1572
+ getOriginalRow(id: TRowId): TRow | undefined;
1573
+ hasOriginalRow(id: TRowId): boolean;
1574
+ trackNonBackendRow(id: TRowId): void;
1575
+ markNew(id: TRowId): void;
1576
+ markEdited(id: TRowId): void;
1577
+ snapshotOriginal(id: TRowId, row: TRow): void;
1578
+ classifyOnFirstEdit(id: TRowId, row: TRow): void;
1579
+ checkRevert(id: TRowId, currentRow: TRow): {
1580
+ wasNew: boolean;
1581
+ wasEdited: boolean;
1582
+ };
1583
+ removeTracking(id: TRowId): void;
1584
+ getNewRowIds(): ReadonlySet<TRowId>;
1585
+ getEditedRowIds(): ReadonlySet<TRowId>;
1586
+ getMergeSnapshot(id: TRowId): {
1587
+ isNew: boolean;
1588
+ isEdited: boolean;
1589
+ };
1590
+ restoreMergeClassification(id: TRowId, isNew: boolean, isEdited: boolean): void;
1163
1591
  clear(): void;
1164
1592
  };
1165
1593
 
1166
- type IValidator<TRow extends DataEditorRow = DataEditorRow> = {
1167
- validateRow(rowId: TRowId): void;
1168
- validateRows(rows: TRow[], rowIds: TRowId[]): void;
1169
- validateUniqueness(): Promise<void>;
1170
- validateCell(rowId: TRowId, field: string, oldValue?: unknown, _visited?: Set<string>): void;
1171
- revalidateColumn(field: string): void;
1172
- validateColumn(field: string, oldValues: ReadonlyMap<TRowId, unknown>): void;
1173
- revalidateColumnChunked(field: string, oldValues: ReadonlyMap<TRowId, unknown>, newValues: ReadonlyMap<TRowId, unknown>, processor: ChunkedProcessor<TRowId>, onComplete: () => void): void;
1174
- removeRow(rowId: TRowId): void;
1175
- destroy(): void;
1176
- };
1177
-
1178
- type IValueIndex<TRow extends DataEditorRow = DataEditorRow> = {
1179
- setTrackedFields(fields: Set<string>): void;
1180
- addRow(row: TRow): void;
1181
- removeRow(row: TRow): void;
1182
- updateField(field: string, oldValue: unknown, newValue: unknown): void;
1183
- rebuild(rows: Iterable<TRow>): void;
1184
- getValues(field: string): ReadonlyMap<string, number>;
1185
- getVersion(): number;
1186
- isTracked(field: string): boolean;
1187
- getMinMax(field: string): {
1188
- min: number;
1189
- max: number;
1190
- } | null;
1191
- getDateMinMax(field: string): {
1192
- min: string;
1193
- max: string;
1194
- } | null;
1195
- bumpVersion(): void;
1196
- };
1197
-
1198
- type RowEntry<TRow extends DataEditorRow> = {
1199
- rowId: TRowId;
1200
- row: TRow;
1201
- sourceId: DataSourceId;
1202
- isNew: boolean;
1203
- isEdited: boolean;
1204
- isDeleted: boolean;
1205
- originalRow?: TRow;
1206
- };
1207
- type OverlayEntry<TRow extends DataEditorRow> = {
1208
- rowId: TRowId;
1209
- displayValue: TRow;
1210
- };
1211
- type SourceSnapshot<TRow extends DataEditorRow> = {
1212
- state: DataSourceState;
1213
- ownedRows: RowEntry<TRow>[];
1214
- overlayRows: OverlayEntry<TRow>[];
1215
- rowIds: TRowId[];
1216
- plan: ExtendedRemovalPlan<TRow>;
1217
- };
1218
- type SourceLifecycleHost<TRow extends DataEditorRow> = {
1219
- getValidator: () => IValidator<TRow> | null;
1220
- pushCommand: (cmd: Command<TRow>, cost?: number) => number;
1221
- notify: () => void;
1222
- isServerStrategy: () => boolean;
1223
- checkRowEmptyCells: (rowId: TRowId) => void;
1224
- clearRowValidations: (rowId: TRowId) => void;
1225
- };
1226
- declare class SourceLifecycle<TRow extends DataEditorRow = DataEditorRow> {
1227
- private readonly sourceManager;
1228
- private readonly rowStore;
1229
- private readonly dirtyTracker;
1230
- private readonly filterEngine;
1231
- private readonly valueIndex;
1232
- private readonly validationStore;
1233
- private readonly snapshotManager;
1234
- private readonly host;
1235
- private readonly importTrackers;
1236
- constructor(sourceManager: SourceManager<TRow>, rowStore: RowStore<TRow>, dirtyTracker: IDirtyTracker<TRow>, filterEngine: IFilterEngine<TRow>, valueIndex: IValueIndex<TRow>, validationStore: IValidationStore, snapshotManager: SnapshotManager, host: SourceLifecycleHost<TRow>);
1237
- plan(sourceId: DataSourceId): ExtendedRemovalPlan<TRow> | null;
1238
- capture(sourceId: DataSourceId): SourceSnapshot<TRow> | null;
1239
- apply(plan: ExtendedRemovalPlan<TRow>): void;
1240
- private applyToStores;
1241
- restore(snapshot: SourceSnapshot<TRow>): void;
1242
- register(options: RegisterSourceOptions): DataSourceId;
1243
- trackAppend(sourceId: DataSourceId, rowIds: TRowId[], rows: TRow[], usedIdMap: boolean): void;
1244
- trackPrimaryKey(sourceId: DataSourceId, primaryKey: keyof TRow): void;
1245
- finalize(sourceId: DataSourceId): Promise<void>;
1246
- private pushImportCommandIfTracked;
1247
- private removeInternal;
1248
- remove(sourceId: DataSourceId): Promise<SourceSnapshot<TRow> | null>;
1249
- finalizeAllSources(): void;
1594
+ interface FlagReader {
1595
+ hasError(id: TRowId): boolean;
1596
+ isNew(id: TRowId): boolean;
1597
+ isEdited(id: TRowId): boolean;
1598
+ hasEmptyCells(id: TRowId): boolean;
1599
+ isDeleted(id: TRowId): boolean;
1600
+ hasDeletedRows(): boolean;
1601
+ getSourceId(id: TRowId): string;
1602
+ getErrorBitmask(id: TRowId, fieldOrder: string[], wordIdx: number): number;
1603
+ getEditedBitmask(id: TRowId, fieldOrder: string[], wordIdx: number): number;
1604
+ getErrorMessageToRows(): ReadonlyMap<string, ReadonlySet<TRowId>>;
1605
+ getRowValidations(id: TRowId): Map<string, NonNullable<ValidationResult>> | undefined;
1606
+ }
1607
+ interface FilterRowReader<TRow extends DataEditorRow = DataEditorRow> {
1608
+ getRowIds(): TRowId[];
1609
+ getRowById(id: TRowId): TRow | undefined;
1610
+ getHiddenSourceIds(): Set<DataSourceId>;
1250
1611
  }
1612
+ type IFilterEngine<TRow extends DataEditorRow = DataEditorRow> = {
1613
+ getFilteredRowIds(): TRowId[] | null;
1614
+ getBaseFilteredRowIds(): TRowId[] | null;
1615
+ getFilterVersion(): number;
1616
+ getFilterCriteriaVersion(): number;
1617
+ isFiltering(): boolean;
1618
+ getFilters(): Filters;
1619
+ getFieldOrder(): string[];
1620
+ getWordsPerRow(): number;
1621
+ getShowOnlyDeletedRows(): boolean;
1622
+ getSortState(): SortState;
1623
+ setReaders(rowReader: FilterRowReader<TRow>, flagReader: FlagReader): void;
1624
+ setColumns(columns: DataEditorColumn[]): void;
1625
+ setFilters(filters: Partial<Filters>): void;
1626
+ setSortState(state: SortState, sortType?: SortType, locales?: string[]): Promise<void>;
1627
+ updateRowText(rowId: TRowId, row: TRow): void;
1628
+ updateRowsText(rows: {
1629
+ id: TRowId;
1630
+ row: TRow;
1631
+ }[]): void;
1632
+ deleteRowTextCache(rowId: TRowId): void;
1633
+ clearRowTextCacheAll(): void;
1634
+ setRowTextCache(rowId: TRowId, row: TRow): void;
1635
+ rebuild(): void;
1636
+ refilterAfterColumnsChange(): void;
1637
+ notifyRowsAdded(ids: TRowId[]): void;
1638
+ notifyRowsDeleted(deletedIds: TRowId[]): void;
1639
+ notifyRowsInserted(restoredIds: TRowId[], positions: number[]): void;
1640
+ refilterAfterSourceToggle(sourceId: DataSourceId, isVisible: boolean): void;
1641
+ refilterAfterFlagChange(): void;
1642
+ flushPendingFlags(): void;
1643
+ markFlagsDirty(): void;
1644
+ testRowAgainstFilters(rowId: TRowId, row: TRow, flagReader: FlagReader, hiddenSourceIds: Set<DataSourceId>): boolean;
1645
+ getAllRowIdsSorted(rowReader: FilterRowReader<TRow>): Promise<TRowId[]>;
1646
+ clear(): void;
1647
+ destroy(): void;
1648
+ };
1251
1649
 
1252
1650
  /**
1253
- * Fill-level delta computations.
1651
+ * RowStore — Row storage with dual-access pattern for the grid engine.
1254
1652
  *
1255
- * Pure functions that compute ColumnDelta[] for fill handle operations.
1256
- * Zero side effects take a spec + row reader, return deltas.
1653
+ * Maintains two parallel structures:
1654
+ * - (TRow | undefined)[] → O(1) lookup by stable internal ID (rows[id])
1655
+ * - TRowId[] → O(1) lookup by visual index (required by the grid)
1257
1656
  *
1258
- * buildFillSpec() reads source values once, builds tiling index
1259
- * computeFillDeltas() — processes a chunk of target rows against the spec
1657
+ * The grid calls getRow(index) on every frame, so index-based access must be
1658
+ * instant. Filtered views are supported by passing an alternate ID array
1659
+ * (filteredIds) to the access methods — the store itself is unaware of filters.
1260
1660
  *
1261
- * Fill uses a tiling pattern: source values repeat cyclically.
1262
- * For row r in fill range: srcRow = r % sourceHeight.
1263
- * For col c in fill range: srcCol = c % sourceWidth.
1661
+ * An inverted index (rowId visual index) is lazily rebuilt on demand for
1662
+ * O(1) reverse lookups (e.g., scrolling to a specific row after undo).
1663
+ *
1664
+ * Internal row IDs are auto-generated as sequential numbers (1, 2, 3, …).
1665
+ * Using a plain array indexed by ID eliminates Map hash table overhead
1666
+ * (~10MB saved at 1M rows). Deleted slots are set to undefined (not delete)
1667
+ * to keep V8 in HOLEY_ELEMENTS mode.
1264
1668
  */
1265
1669
 
1266
- type FillSpec = {
1267
- /** 2D source grid: sourceValues[row][col]. Read once — source region is always small. */
1268
- sourceValues: unknown[][];
1269
- sourceHeight: number;
1270
- sourceWidth: number;
1271
- /** Column IDs for the fill target columns. */
1272
- fields: string[];
1273
- /** Target rowId position within the fill range, for tiling modulo. */
1274
- rowIdToFillIndex: ReadonlyMap<TRowId, number>;
1275
- };
1276
-
1277
- type FormulaCellContext = {
1278
- value: unknown;
1279
- field: string;
1280
- rowId: TRowId;
1281
- getField: (field: string) => unknown;
1282
- /**
1283
- * Positional arguments for expression-compiled formulas.
1284
- * Populated by the expression evaluator when invoking multi-arg function
1285
- * calls. Single-arg formulas (UPPER, TRIM, etc.) still read from `value`.
1286
- * Undefined for all non-expression call sites — existing code is unaffected.
1287
- */
1288
- args?: readonly unknown[];
1289
- };
1290
- type FormulaParamType = "string" | "number" | "boolean" | "select";
1291
- type FormulaParam = {
1292
- name: string;
1293
- label: string;
1294
- type: FormulaParamType;
1295
- required?: boolean;
1296
- defaultValue?: unknown;
1297
- options?: Array<{
1298
- id: string;
1299
- text: string;
1300
- }>;
1301
- };
1302
- type ColumnInputKind = "single" | "multiple";
1303
- type ColumnInput = {
1304
- name: string;
1305
- label: string;
1306
- kind: ColumnInputKind;
1307
- required?: boolean;
1308
- };
1309
- type FormulaCategory = "text" | "number" | "logic" | "custom";
1310
- type FormulaArity = {
1311
- /** Minimum number of positional arguments. 0 means "callable with no args". */
1312
- min: number;
1313
- /** Maximum number of positional arguments. Use Number.POSITIVE_INFINITY for variadic. */
1314
- max: number;
1315
- };
1316
- type FormulaBase = {
1317
- name: string;
1318
- label: string;
1319
- category: FormulaCategory;
1320
- description?: string;
1321
- columns?: ColumnInput[];
1322
- params: FormulaParam[];
1323
- /**
1324
- * The call signature of this formula when invoked from the expression
1325
- * language. Required. For shortcut formulas (UPPER, TRIM, ...) use
1326
- * { min: 1, max: 1 }. For CLEAR use { min: 0, max: 0 }. For MERGE use
1327
- * { min: 2, max: Number.POSITIVE_INFINITY }.
1328
- */
1329
- arity: FormulaArity;
1330
- /** Parameter signature shown in autocomplete, without the function name. e.g. "(text, count)" */
1331
- syntax?: string;
1332
- /**
1333
- * Whether this formula may be invoked from the expression parser.
1334
- * Defaults to true when omitted. MERGE and SPLIT set this to false
1335
- * because they have dedicated modals that supply their non-expression
1336
- * params (column lists, separator, ...).
1337
- */
1338
- expressionCallable?: boolean;
1339
- };
1340
- type CellFormula = FormulaBase & {
1341
- kind: "cell";
1342
- compute: (ctx: FormulaCellContext, params: Record<string, unknown>) => unknown;
1343
- };
1344
- type RowFormula = FormulaBase & {
1345
- kind: "row";
1346
- targetFields: (params: Record<string, unknown>) => string[];
1347
- compute: (ctx: FormulaCellContext, params: Record<string, unknown>) => Record<string, unknown>;
1348
- };
1349
- type FormulaDefinition = CellFormula | RowFormula;
1350
-
1351
- declare class FormulaRegistry {
1352
- private readonly formulas;
1353
- register(formula: FormulaDefinition): void;
1354
- get(name: string): FormulaDefinition | undefined;
1355
- getAll(): FormulaDefinition[];
1356
- getByCategory(category: string): FormulaDefinition[];
1357
- getExpressionCallable(): FormulaDefinition[];
1670
+ declare class RowStore<TRow extends DataEditorRow = DataEditorRow> {
1671
+ private rows;
1672
+ private rowIds;
1673
+ private rowIdCounter;
1674
+ private rowIdToIndex;
1675
+ private _rowIndexDirty;
1676
+ nextRowId(): TRowId;
1677
+ getRow(index: number, filteredIds: TRowId[] | null): TRow | undefined;
1678
+ getRowById(id: TRowId): TRow | undefined;
1679
+ getRowId(index: number, filteredIds: TRowId[] | null): TRowId | undefined;
1680
+ getRowIndex(rowId: TRowId, filteredIds: TRowId[] | null): number;
1681
+ getRowIds(): TRowId[];
1682
+ getRowCount(): number;
1683
+ allRows(): IterableIterator<TRow>;
1684
+ hasRow(id: TRowId): boolean;
1685
+ setRow(id: TRowId, row: TRow): void;
1686
+ deleteRow(id: TRowId): void;
1687
+ pushRowId(id: TRowId): void;
1688
+ spliceRowId(pos: number, id: TRowId): void;
1689
+ removeRowId(id: TRowId): void;
1690
+ filterRowIds(predicate: (id: TRowId) => boolean): void;
1691
+ trimFromStart(count: number): TRowId[];
1692
+ trimFromEnd(count: number): TRowId[];
1693
+ unshiftRowIds(ids: TRowId[]): void;
1694
+ invalidateIndex(): void;
1695
+ clear(): void;
1696
+ private rebuildRowIdToIndex;
1358
1697
  }
1359
1698
 
1360
1699
  /**
1361
- * Paste-level delta computations.
1700
+ * SnapshotManager Immutable snapshot construction and listener notification
1701
+ * for React's useSyncExternalStore integration.
1362
1702
  *
1363
- * Pure functions that compute ColumnDelta[] for paste operations.
1364
- * Zero side effects take a spec + row reader, return deltas.
1703
+ * Builds a DataStoreSnapshot object that React components subscribe to via
1704
+ * useSyncExternalStore(subscribe, getSnapshot). A new snapshot object is
1705
+ * created on every notify() call, which triggers React's shallow comparison
1706
+ * and re-renders only when values actually change.
1365
1707
  *
1366
- * buildPasteSpec() — pre-resolves the source→target row mapping and column metadata
1367
- * computePasteDeltas() processes a chunk of target rows against the spec
1708
+ * Count tracking has two tiers:
1709
+ * - Visible counts: new/edited/error/empty rows from visible sources only.
1710
+ * Updated incrementally via adjust*() methods on single-row edits, or
1711
+ * recomputed in bulk when _countsDirty is set (after deletes, source changes, etc.).
1712
+ * - Filtered counts: subset of visible counts restricted to filteredRowIds.
1713
+ * Recomputed when _filteredCountsDirty is set (after filter changes).
1368
1714
  *
1369
- * Designed for the same chunked execution pattern as columnTransforms:
1370
- * - DataStore builds the spec once
1371
- * - Small datasets: call computePasteDeltas() once with all target rows
1372
- * - Large datasets: orchestrator calls computePasteDeltas() per chunk
1715
+ * Batching: multiple operations can be grouped via beginBatch()/endBatch().
1716
+ * While batching, notify() calls are deferred — only the outermost endBatch()
1717
+ * triggers a single notification, preventing intermediate snapshots from
1718
+ * reaching React during multi-step operations (fill handle, batch delete, etc.).
1719
+ *
1720
+ * The SnapshotStateReader interface decouples this module from all other modules.
1721
+ * DataStore implements the interface by delegating to RowStore, DirtyTracker,
1722
+ * FilterEngine, and ValidationStore, so SnapshotManager never imports them directly.
1373
1723
  */
1374
1724
 
1375
- type PasteSpec = {
1376
- sourceColumnIds: string[];
1377
- targetColumnIds: string[];
1378
- targetToSource: ReadonlyMap<TRowId, TRowId>;
1379
- selectOptionsMap: ReadonlyMap<string, ReadonlySet<string>>;
1380
- skipColumnIndices: ReadonlySet<number>;
1381
- isCut: boolean;
1382
- };
1725
+ type Listener = () => void;
1726
+ interface SnapshotStateReader {
1727
+ getRowCount(): number;
1728
+ getFilteredRowIds(): TRowId[] | null;
1729
+ getBaseFilteredRowIds(): TRowId[] | null;
1730
+ getNewRowIds(): ReadonlySet<TRowId>;
1731
+ getEditedRowIds(): ReadonlySet<TRowId>;
1732
+ getRowsWithErrors(): ReadonlySet<TRowId>;
1733
+ getRowsWithEmptyCells(): ReadonlySet<TRowId>;
1734
+ getDeletedCount(): number;
1735
+ isDeleted(id: TRowId): boolean;
1736
+ getSources(): DataSourceState[];
1737
+ isRowVisible(id: TRowId): boolean;
1738
+ canUndo(): boolean;
1739
+ canRedo(): boolean;
1740
+ isLoading(): boolean;
1741
+ isFiltering(): boolean;
1742
+ getVersion(): number;
1743
+ getFilterCriteriaVersion(): number;
1744
+ getFilteredNewCount(ids: TRowId[] | null): number;
1745
+ getFilteredEditedCount(ids: TRowId[] | null): number;
1746
+ getFilteredDirtyCount(ids: TRowId[] | null): number;
1747
+ getFilteredErrorCount(ids: TRowId[] | null): number;
1748
+ getFilteredEmptyCount(ids: TRowId[] | null): number;
1749
+ getErrorMessageCounts(filteredRowIds: TRowId[] | null): Record<string, number>;
1750
+ hasColumnScoping(): boolean;
1751
+ getSortState(): SortState;
1752
+ getShowOnlyDeletedRows(): boolean;
1753
+ /** Server-provided aggregate counts. Present only in server mode. */
1754
+ getServerEditedCount?(): number;
1755
+ getServerNewCount?(): number;
1756
+ getServerErrorCount?(): number;
1757
+ getServerEmptyCount?(): number;
1758
+ getServerDeletedCount?(): number;
1759
+ }
1760
+ declare class SnapshotManager {
1761
+ private _visibleNewCount;
1762
+ private _visibleEditedCount;
1763
+ private _visibleDirtyCount;
1764
+ private _visibleErrorCount;
1765
+ private _visibleEmptyCount;
1766
+ private _countsDirty;
1767
+ private _filteredDirtyCount;
1768
+ private _filteredNewCount;
1769
+ private _filteredEditedCount;
1770
+ private _filteredErrorCount;
1771
+ private _filteredEmptyCount;
1772
+ private _errorMessageCounts;
1773
+ private _filteredCountsDirty;
1774
+ private _phase;
1775
+ private _processingInfo;
1776
+ private snapshot;
1777
+ private listeners;
1778
+ private batchDepth;
1779
+ private pendingNotify;
1780
+ get countsDirty(): boolean;
1781
+ markCountsDirty(): void;
1782
+ markFilteredCountsDirty(): void;
1783
+ adjustVisibleErrorCount(delta: number): void;
1784
+ adjustVisibleNewCount(delta: number): void;
1785
+ adjustVisibleEditedCount(delta: number): void;
1786
+ adjustVisibleDirtyCount(delta: number): void;
1787
+ subscribe: (listener: Listener) => (() => void);
1788
+ getSnapshot: () => DataStoreSnapshot;
1789
+ isBatching(): boolean;
1790
+ beginBatch(): boolean;
1791
+ endBatch(): boolean;
1792
+ setPhase(phase: ProcessingPhase, info?: ProcessingInfo): void;
1793
+ getPhase(): ProcessingPhase;
1794
+ notify(reader: SnapshotStateReader): void;
1795
+ clear(): void;
1796
+ clearListeners(): void;
1797
+ private recomputeVisibleCounts;
1798
+ private recomputeFilteredCounts;
1799
+ }
1383
1800
 
1384
1801
  /**
1385
- * Base row shape. Each key is a column ID, each value is the cell data.
1386
- * Extend this with your own type via the `<DataEditor<TRow>>` generic for
1387
- * type-safe column access. When the generic is omitted, rows are typed as
1388
- * `Record<string, unknown>`.
1802
+ * ValidationStore Cell-level validation state with incremental count tracking.
1389
1803
  *
1390
- * @example
1391
- * ```ts
1392
- * type Employee = { id: string; name: string; email: string };
1393
- * <DataEditor<Employee> columns={...} primaryKey="id" />
1394
- * ```
1395
- */
1396
- type DataEditorRow = Record<string, unknown>;
1397
- /** Sort direction. */
1398
- type SortDirection = "asc" | "desc";
1399
- /** Current sort state. `null` means no active sort. */
1400
- type SortState = {
1401
- columnId: string;
1402
- direction: SortDirection;
1403
- } | null;
1404
- type Filters = {
1405
- search: string;
1406
- matchCase: boolean;
1407
- matchEntireCell: boolean;
1408
- errorMessageFilters: string[];
1409
- showOnlyNewRows: boolean;
1410
- showOnlyEditedRows: boolean;
1411
- showOnlyEmptyCells: boolean;
1412
- /** When true, show only rows flagged for deletion (bin mode). All other filters are bypassed. */
1413
- showOnlyDeletedRows: boolean;
1414
- filterColumns: string[] | null;
1415
- /** Per-column value filters. Key = column ID, value = allowed display-formatted strings. */
1416
- columnValueFilters: Record<string, string[]>;
1417
- /** Per-column numeric range filters. Key = column ID. */
1418
- columnRangeFilters: Record<string, {
1419
- min?: number;
1420
- max?: number;
1421
- }>;
1422
- /** Per-column date range filters. Key = column ID, values are ISO date strings (YYYY-MM-DD). */
1423
- columnDateRangeFilters: Record<string, {
1424
- min?: string;
1425
- max?: string;
1426
- }>;
1427
- };
1428
-
1429
- /**
1430
- * A single operation the LLM wants to apply to rows in the current filtered view.
1804
+ * Stores validation results in a two-level map: rowId → fieldId → ValidationResult[].
1805
+ * Maintains a running error count and a pre-computed row-level set
1806
+ * (_rowsWithErrors) for O(1) "does this row have errors?" checks.
1431
1807
  *
1432
- * - `edit` `fn` is `(r, ctx) => void`. Mutates `r` in place. Changed fields
1433
- * become column deltas. Rows with no changes are no-ops.
1434
- * - `delete` `fn` is `(r, ctx) => boolean`. Truthy means "flag this row for
1435
- * deletion". Soft delete via `DeleteRowCommand`.
1436
- */
1437
- type ChatOp = {
1438
- action: "edit";
1439
- fn: string;
1440
- } | {
1441
- action: "delete";
1442
- fn: string;
1443
- };
1444
- /**
1445
- * A single chunk in the stream returned from `DataEditorChat.onMessage`.
1808
+ * The key design decision is the ValidationDelta return type from setCellValidation().
1809
+ * Instead of directly triggering notifications, it returns a delta object describing
1810
+ * what changed (error count +/- 1, row-level error membership flipped).
1811
+ * The caller (DataStore) uses this delta for incremental snapshot count updates,
1812
+ * avoiding a full recount on every validation change.
1446
1813
  *
1447
- * - `status` progress message shown while processing (e.g. "Analyzing 500 rows...").
1448
- * - `message` chat reply shown to the user.
1449
- * - `rows` — updated rows to apply to the grid. Matched by `primaryKey`.
1450
- * - `ops` — array of per-row operations (edits and/or deletes) to apply in order.
1814
+ * Empty cell tracking (_rowsWithEmptyCells) is separate from validation it
1815
+ * tracks rows where any visible column has a null/empty value, used by the
1816
+ * "show only rows with empty cells" filter.
1451
1817
  */
1452
- type ChatResponseChunk<TRow extends DataEditorRow = DataEditorRow> = {
1453
- type: "status";
1454
- content: string;
1455
- } | {
1456
- type: "message";
1457
- content: string;
1458
- } | {
1459
- type: "rows";
1460
- content: TRow[];
1461
- } | {
1462
- type: "ops";
1463
- content: ChatOp[];
1818
+
1819
+ type ValidationDelta = {
1820
+ errorDelta: number;
1821
+ rowErrorChanged: boolean;
1464
1822
  };
1465
- /** Status of a row in the chat sample, relative to its origin snapshot. */
1466
- type ChatRowStatus = "new" | "edited" | "original";
1467
- /** A sample row handed to the chat callback, with its current status and validation errors. */
1468
- type ChatRow<TRow extends DataEditorRow = DataEditorRow> = {
1469
- /** Row data keyed by column ID. */
1470
- data: TRow;
1471
- /** Whether the row was newly added, edited, or is unchanged. */
1472
- status: ChatRowStatus;
1473
- /** Validation errors keyed by column ID. */
1474
- errors: Record<string, string[]>;
1475
- /** The source this row belongs to. */
1476
- source: string;
1823
+ type IValidationStore = {
1824
+ setCellValidation(rowId: TRowId, field: string, result: ValidationResult): ValidationDelta;
1825
+ getCellValidation(rowId: TRowId, field: string): ValidationResult;
1826
+ clearRowValidations(rowId: TRowId): void;
1827
+ hasRowErrors(rowId: TRowId): boolean;
1828
+ getRowsWithErrors(): ReadonlySet<TRowId>;
1829
+ getRowsWithEmptyCells(): ReadonlySet<TRowId>;
1830
+ hasEmptyCells(rowId: TRowId): boolean;
1831
+ checkRowEmptyCells(rowId: TRowId, row: Record<string, unknown> | undefined, fieldOrder: string[]): boolean;
1832
+ deleteRowTracking(rowId: TRowId): void;
1833
+ getErrorCount(): number;
1834
+ getRowValidations(rowId: TRowId): Map<string, NonNullable<ValidationResult>> | undefined;
1835
+ getErrorMessageToRows(): ReadonlyMap<string, ReadonlySet<TRowId>>;
1836
+ clear(): void;
1477
1837
  };
1478
- /** Aggregated error count across the current view, grouped by field and message. */
1479
- type ChatErrorSummary = {
1480
- /** Column ID where the error occurred. */
1481
- field: string;
1482
- /** The validation message. */
1483
- message: string;
1484
- /** How many rows hit this error. */
1485
- count: number;
1486
- /** A few example values that triggered the error. */
1487
- examples: string[];
1838
+
1839
+ type IValidator<TRow extends DataEditorRow = DataEditorRow> = {
1840
+ validateRow(rowId: TRowId): void;
1841
+ validateRows(rows: TRow[], rowIds: TRowId[]): void;
1842
+ validateUniqueness(): Promise<void>;
1843
+ validateCell(rowId: TRowId, field: string, oldValue?: unknown, _visited?: Set<string>): void;
1844
+ revalidateColumn(field: string): void;
1845
+ validateColumn(field: string, oldValues: ReadonlyMap<TRowId, unknown>): void;
1846
+ revalidateColumnChunked(field: string, oldValues: ReadonlyMap<TRowId, unknown>, newValues: ReadonlyMap<TRowId, unknown>, processor: ChunkedProcessor<TRowId>, onComplete: () => void): void;
1847
+ removeRow(rowId: TRowId): void;
1848
+ destroy(): void;
1488
1849
  };
1489
- /**
1490
- * Context about the current dataset, passed to `loadSuggestions` and extended
1491
- * into `ChatContext` for `onMessage`.
1492
- */
1493
- type ChatDataContext<TRow extends DataEditorRow = DataEditorRow> = {
1494
- /** Full column definitions. */
1495
- columns: DataEditorColumn[];
1496
- /** Row identifier field. */
1497
- primaryKey: keyof TRow;
1498
- /** Total rows in the dataset. */
1499
- totalRowCount: number;
1500
- /** Rows in the current filtered view. */
1501
- filteredRowCount: number;
1502
- /** Sample rows, with status and errors. Size controlled by `sampleSize`. */
1503
- sample: ChatRow<TRow>[];
1504
- /** Aggregated error counts by field and message. */
1505
- errorSummary: ChatErrorSummary[];
1506
- /** Access all rows. Use for full-dataset operations. */
1507
- getRows: () => ChatRow<TRow>[];
1850
+
1851
+ type IValueIndex<TRow extends DataEditorRow = DataEditorRow> = {
1852
+ setTrackedFields(fields: Set<string>): void;
1853
+ addRow(row: TRow): void;
1854
+ removeRow(row: TRow): void;
1855
+ updateField(field: string, oldValue: unknown, newValue: unknown): void;
1856
+ rebuild(rows: Iterable<TRow>): void;
1857
+ getValues(field: string): ReadonlyMap<string, number>;
1858
+ getVersion(): number;
1859
+ isTracked(field: string): boolean;
1860
+ getMinMax(field: string): {
1861
+ min: number;
1862
+ max: number;
1863
+ } | null;
1864
+ getDateMinMax(field: string): {
1865
+ min: string;
1866
+ max: string;
1867
+ } | null;
1868
+ bumpVersion(): void;
1508
1869
  };
1509
- /**
1510
- * The full context passed to `DataEditorChat.onMessage` when the user sends
1511
- * a prompt. Includes the prompt itself and all dataset context.
1512
- */
1513
- type ChatContext<TRow extends DataEditorRow = DataEditorRow> = ChatDataContext<TRow> & {
1514
- /** The user's chat prompt. */
1515
- message: string;
1870
+
1871
+ type RowEntry<TRow extends DataEditorRow> = {
1872
+ rowId: TRowId;
1873
+ row: TRow;
1874
+ sourceId: DataSourceId;
1875
+ isNew: boolean;
1876
+ isEdited: boolean;
1877
+ isDeleted: boolean;
1878
+ originalRow?: TRow;
1879
+ };
1880
+ type OverlayEntry<TRow extends DataEditorRow> = {
1881
+ rowId: TRowId;
1882
+ displayValue: TRow;
1516
1883
  };
1884
+ type SourceSnapshot<TRow extends DataEditorRow> = {
1885
+ state: DataSourceState;
1886
+ ownedRows: RowEntry<TRow>[];
1887
+ overlayRows: OverlayEntry<TRow>[];
1888
+ rowIds: TRowId[];
1889
+ plan: ExtendedRemovalPlan<TRow>;
1890
+ };
1891
+ type SourceLifecycleHost<TRow extends DataEditorRow> = {
1892
+ getValidator: () => IValidator<TRow> | null;
1893
+ pushCommand: (cmd: Command<TRow>, cost?: number) => number;
1894
+ notify: () => void;
1895
+ isServerStrategy: () => boolean;
1896
+ checkRowEmptyCells: (rowId: TRowId) => void;
1897
+ clearRowValidations: (rowId: TRowId) => void;
1898
+ };
1899
+ declare class SourceLifecycle<TRow extends DataEditorRow = DataEditorRow> {
1900
+ private readonly sourceManager;
1901
+ private readonly rowStore;
1902
+ private readonly dirtyTracker;
1903
+ private readonly filterEngine;
1904
+ private readonly valueIndex;
1905
+ private readonly validationStore;
1906
+ private readonly snapshotManager;
1907
+ private readonly host;
1908
+ private readonly importTrackers;
1909
+ constructor(sourceManager: SourceManager<TRow>, rowStore: RowStore<TRow>, dirtyTracker: IDirtyTracker<TRow>, filterEngine: IFilterEngine<TRow>, valueIndex: IValueIndex<TRow>, validationStore: IValidationStore, snapshotManager: SnapshotManager, host: SourceLifecycleHost<TRow>);
1910
+ plan(sourceId: DataSourceId): ExtendedRemovalPlan<TRow> | null;
1911
+ capture(sourceId: DataSourceId): SourceSnapshot<TRow> | null;
1912
+ apply(plan: ExtendedRemovalPlan<TRow>): void;
1913
+ private applyToStores;
1914
+ restore(snapshot: SourceSnapshot<TRow>): void;
1915
+ register(options: RegisterSourceOptions): DataSourceId;
1916
+ trackAppend(sourceId: DataSourceId, rowIds: TRowId[], rows: TRow[], usedIdMap: boolean): void;
1917
+ trackPrimaryKey(sourceId: DataSourceId, primaryKey: keyof TRow): void;
1918
+ finalize(sourceId: DataSourceId): Promise<void>;
1919
+ private pushImportCommandIfTracked;
1920
+ private removeInternal;
1921
+ remove(sourceId: DataSourceId): Promise<SourceSnapshot<TRow> | null>;
1922
+ finalizeAllSources(): void;
1923
+ }
1924
+
1517
1925
  /**
1518
- * Bring-your-own-AI chat configuration. When provided via the `chat` prop,
1519
- * the editor shows a chat panel alongside the grid. You own the AI
1520
- * integration; the SDK hands you dataset context and renders the streamed
1521
- * response.
1926
+ * Fill-level delta computations.
1522
1927
  *
1523
- * @example
1524
- * ```ts
1525
- * chat={{
1526
- * sampleSize: 50,
1527
- * onMessage: async function* (context) {
1528
- * yield { type: "status", content: "Thinking..." };
1529
- * const res = await fetch("/api/ai", {
1530
- * method: "POST",
1531
- * body: JSON.stringify({
1532
- * prompt: context.message,
1533
- * columns: context.columns,
1534
- * sample: context.sample.map(r => r.data),
1535
- * errors: context.errorSummary,
1536
- * }),
1537
- * }).then(r => r.json());
1538
- * yield { type: "rows", content: res.updatedRows };
1539
- * yield { type: "ops", content: res.ops };
1540
- * yield { type: "message", content: res.reply };
1541
- * },
1542
- * }}
1543
- * ```
1928
+ * Pure functions that compute ColumnDelta[] for fill handle operations.
1929
+ * Zero side effects — take a spec + row reader, return deltas.
1930
+ *
1931
+ * buildFillSpec() — reads source values once, builds tiling index
1932
+ * computeFillDeltas() processes a chunk of target rows against the spec
1933
+ *
1934
+ * Fill uses a tiling pattern: source values repeat cyclically.
1935
+ * For row r in fill range: srcRow = r % sourceHeight.
1936
+ * For col c in fill range: srcCol = c % sourceWidth.
1544
1937
  */
1545
- type DataEditorChat<TRow extends DataEditorRow = DataEditorRow> = {
1938
+
1939
+ type FillSpec = {
1940
+ /** 2D source grid: sourceValues[row][col]. Read once — source region is always small. */
1941
+ sourceValues: unknown[][];
1942
+ sourceHeight: number;
1943
+ sourceWidth: number;
1944
+ /** Column IDs for the fill target columns. */
1945
+ fields: string[];
1946
+ /** Target rowId → position within the fill range, for tiling modulo. */
1947
+ rowIdToFillIndex: ReadonlyMap<TRowId, number>;
1948
+ };
1949
+
1950
+ type FormulaCellContext = {
1951
+ value: unknown;
1952
+ field: string;
1953
+ rowId: TRowId;
1954
+ getField: (field: string) => unknown;
1546
1955
  /**
1547
- * How many rows to include in the context sample. The SDK picks a
1548
- * representative slice including rows with errors. When omitted, the SDK decides.
1956
+ * Positional arguments for expression-compiled formulas.
1957
+ * Populated by the expression evaluator when invoking multi-arg function
1958
+ * calls. Single-arg formulas (UPPER, TRIM, etc.) still read from `value`.
1959
+ * Undefined for all non-expression call sites — existing code is unaffected.
1549
1960
  */
1550
- sampleSize?: number;
1551
- /** Title shown above the suggestion list when the chat is empty. */
1552
- emptyTitle?: string;
1553
- /** How many suggestions to request from `loadSuggestions`. */
1554
- suggestionsCount?: number;
1555
- /** Optional prompt generator for the empty-state suggestion chips. */
1556
- loadSuggestions?: (context: ChatDataContext<TRow>) => Promise<string[]>;
1961
+ args?: readonly unknown[];
1962
+ };
1963
+ type FormulaParamType = "string" | "number" | "boolean" | "select";
1964
+ type FormulaParam = {
1965
+ name: string;
1966
+ label: string;
1967
+ type: FormulaParamType;
1968
+ required?: boolean;
1969
+ defaultValue?: unknown;
1970
+ options?: Array<{
1971
+ id: string;
1972
+ text: string;
1973
+ }>;
1974
+ };
1975
+ type ColumnInputKind = "single" | "multiple";
1976
+ type ColumnInput = {
1977
+ name: string;
1978
+ label: string;
1979
+ kind: ColumnInputKind;
1980
+ required?: boolean;
1981
+ };
1982
+ type FormulaCategory = "text" | "number" | "logic" | "custom";
1983
+ type FormulaArity = {
1984
+ /** Minimum number of positional arguments. 0 means "callable with no args". */
1985
+ min: number;
1986
+ /** Maximum number of positional arguments. Use Number.POSITIVE_INFINITY for variadic. */
1987
+ max: number;
1988
+ };
1989
+ type FormulaBase = {
1990
+ name: string;
1991
+ label: string;
1992
+ category: FormulaCategory;
1993
+ description?: string;
1994
+ columns?: ColumnInput[];
1995
+ params: FormulaParam[];
1557
1996
  /**
1558
- * Called when the user sends a message. Receives full dataset context.
1559
- * Returns an async iterable of response chunks the SDK streams them
1560
- * into the UI.
1997
+ * The call signature of this formula when invoked from the expression
1998
+ * language. Required. For shortcut formulas (UPPER, TRIM, ...) use
1999
+ * { min: 1, max: 1 }. For CLEAR use { min: 0, max: 0 }. For MERGE use
2000
+ * { min: 2, max: Number.POSITIVE_INFINITY }.
1561
2001
  */
1562
- onMessage: (context: ChatContext<TRow>) => AsyncIterable<ChatResponseChunk<TRow>>;
1563
- /** Called when the user cancels a pending request. Use to abort your API call. */
1564
- onCancel?: () => void;
1565
- };
1566
-
1567
- type ApplyFormulaOptions = {
2002
+ arity: FormulaArity;
2003
+ /** Parameter signature shown in autocomplete, without the function name. e.g. "(text, count)" */
2004
+ syntax?: string;
1568
2005
  /**
1569
- * Column IDs to delete AFTER the formula has been applied.
1570
- * Only dynamic (user-added) columns are actually deleted; schema columns
1571
- * in this list are silently ignored. All operations (formula + deletes)
1572
- * are wrapped in a single CompoundCommand so that one Undo call restores
1573
- * every change, regardless of how many columns were listed.
2006
+ * Whether this formula may be invoked from the expression parser.
2007
+ * Defaults to true when omitted. MERGE and SPLIT set this to false
2008
+ * because they have dedicated modals that supply their non-expression
2009
+ * params (column lists, separator, ...).
1574
2010
  */
1575
- readonly deleteColumnsAfter?: readonly string[];
2011
+ expressionCallable?: boolean;
1576
2012
  };
1577
- declare class DataStore<TRow extends DataEditorRow = DataEditorRow> {
1578
- private readonly _mode;
1579
- isServer(): boolean;
1580
- isClient(): boolean;
1581
- private rowStore;
1582
- readonly formulaRegistry: FormulaRegistry;
1583
- private _isLoading;
1584
- private sourceManager;
1585
- readonly sourceLifecycle: SourceLifecycle<TRow>;
1586
- private dirtyTracker;
1587
- private filterEngine;
1588
- private validationStore;
1589
- private snapshotManager;
1590
- private history;
1591
- private validator;
1592
- private valueIndex;
1593
- private serverCounts;
1594
- private editBuilder;
1595
- private isUndoRedoing;
1596
- private pendingBatchCommands;
1597
- private _version;
1598
- private _skipNotify;
1599
- private _bulkMode;
1600
- private _editedCells;
1601
- private _primaryKey;
1602
- /** Columns that are currently locked (pre-locked from schema + user-locked at runtime). */
1603
- private _lockedColumns;
1604
- /** Columns locked via schema definition user cannot unlock these. */
1605
- private _preLockedColumns;
1606
- /** Last-known visible row range reported by the canvas scroll handler. */
1607
- private _viewportStart;
1608
- private _viewportEnd;
1609
- private pendingMutations;
1610
- private editParamsHistory;
1611
- readonly errorHandler: ErrorHandler;
1612
- readonly server: ServerDataManager<TRow> | null;
1613
- private readonly strategy;
1614
- private readonly serverStrategy;
1615
- private snapshotReader;
2013
+ type CellFormula = FormulaBase & {
2014
+ kind: "cell";
2015
+ compute: (ctx: FormulaCellContext, params: Record<string, unknown>) => unknown;
2016
+ };
2017
+ type RowFormula = FormulaBase & {
2018
+ kind: "row";
2019
+ targetFields: (params: Record<string, unknown>) => string[];
2020
+ compute: (ctx: FormulaCellContext, params: Record<string, unknown>) => Record<string, unknown>;
2021
+ };
2022
+ type FormulaDefinition = CellFormula | RowFormula;
2023
+
2024
+ declare class FormulaRegistry {
2025
+ private readonly formulas;
2026
+ register(formula: FormulaDefinition): void;
2027
+ get(name: string): FormulaDefinition | undefined;
2028
+ getAll(): FormulaDefinition[];
2029
+ getByCategory(category: string): FormulaDefinition[];
2030
+ getExpressionCallable(): FormulaDefinition[];
2031
+ }
2032
+
2033
+ /**
2034
+ * Paste-level delta computations.
2035
+ *
2036
+ * Pure functions that compute ColumnDelta[] for paste operations.
2037
+ * Zero side effects — take a spec + row reader, return deltas.
2038
+ *
2039
+ * buildPasteSpec() — pre-resolves the source→target row mapping and column metadata
2040
+ * computePasteDeltas() — processes a chunk of target rows against the spec
2041
+ *
2042
+ * Designed for the same chunked execution pattern as columnTransforms:
2043
+ * - DataStore builds the spec once
2044
+ * - Small datasets: call computePasteDeltas() once with all target rows
2045
+ * - Large datasets: orchestrator calls computePasteDeltas() per chunk
2046
+ */
2047
+
2048
+ type PasteSpec = {
2049
+ sourceColumnIds: string[];
2050
+ targetColumnIds: string[];
2051
+ targetToSource: ReadonlyMap<TRowId, TRowId>;
2052
+ selectOptionsMap: ReadonlyMap<string, ReadonlySet<string>>;
2053
+ skipColumnIndices: ReadonlySet<number>;
2054
+ isCut: boolean;
2055
+ };
2056
+
2057
+ /**
2058
+ * A single operation the LLM wants to apply to rows in the current filtered view.
2059
+ *
2060
+ * - `edit` — `fn` is `(r, ctx) => void`. Mutates `r` in place. Changed fields
2061
+ * become column deltas. Rows with no changes are no-ops.
2062
+ * - `delete` — `fn` is `(r, ctx) => boolean`. Truthy means "flag this row for
2063
+ * deletion". Soft delete via `DeleteRowCommand`.
2064
+ */
2065
+ type ChatOp = {
2066
+ action: "edit";
2067
+ fn: string;
2068
+ } | {
2069
+ action: "delete";
2070
+ fn: string;
2071
+ };
2072
+ /**
2073
+ * A single chunk in the stream returned from `DataEditorChat.onMessage`.
2074
+ *
2075
+ * - `status` — progress message shown while processing (e.g. "Analyzing 500 rows...").
2076
+ * - `message` — chat reply shown to the user.
2077
+ * - `rows` — updated rows to apply to the grid. Matched by `primaryKey`.
2078
+ * - `ops` — array of per-row operations (edits and/or deletes) to apply in order.
2079
+ */
2080
+ type ChatResponseChunk<TRow extends DataEditorRow = DataEditorRow> = {
2081
+ type: "status";
2082
+ content: string;
2083
+ } | {
2084
+ type: "message";
2085
+ content: string;
2086
+ } | {
2087
+ type: "rows";
2088
+ content: TRow[];
2089
+ } | {
2090
+ type: "ops";
2091
+ content: ChatOp[];
2092
+ };
2093
+ /** Status of a row in the chat sample, relative to its origin snapshot. */
2094
+ type ChatRowStatus = "new" | "edited" | "original";
2095
+ /** A sample row handed to the chat callback, with its current status and validation errors. */
2096
+ type ChatRow<TRow extends DataEditorRow = DataEditorRow> = {
2097
+ /** Row data keyed by column ID. */
2098
+ data: TRow;
2099
+ /** Whether the row was newly added, edited, or is unchanged. */
2100
+ status: ChatRowStatus;
2101
+ /** Validation errors keyed by column ID. */
2102
+ errors: Record<string, string[]>;
2103
+ /** The source this row belongs to. */
2104
+ source: string;
2105
+ };
2106
+ /** Aggregated error count across the current view, grouped by field and message. */
2107
+ type ChatErrorSummary = {
2108
+ /** Column ID where the error occurred. */
2109
+ field: string;
2110
+ /** The validation message. */
2111
+ message: string;
2112
+ /** How many rows hit this error. */
2113
+ count: number;
2114
+ /** A few example values that triggered the error. */
2115
+ examples: string[];
2116
+ };
2117
+ /**
2118
+ * Context about the current dataset, passed to `loadSuggestions` and extended
2119
+ * into `ChatContext` for `onMessage`.
2120
+ */
2121
+ type ChatDataContext<TRow extends DataEditorRow = DataEditorRow> = {
2122
+ /** Full column definitions. */
2123
+ columns: DataEditorColumn[];
2124
+ /** Row identifier field. */
2125
+ primaryKey: keyof TRow;
2126
+ /** Total rows in the dataset. */
2127
+ totalRowCount: number;
2128
+ /** Rows in the current filtered view. */
2129
+ filteredRowCount: number;
2130
+ /** Sample rows, with status and errors. Size controlled by `sampleSize`. */
2131
+ sample: ChatRow<TRow>[];
2132
+ /** Aggregated error counts by field and message. */
2133
+ errorSummary: ChatErrorSummary[];
2134
+ /** Access all rows. Use for full-dataset operations. */
2135
+ getRows: () => ChatRow<TRow>[];
2136
+ };
2137
+ /**
2138
+ * The full context passed to `DataEditorChat.onMessage` when the user sends
2139
+ * a prompt. Includes the prompt itself and all dataset context.
2140
+ */
2141
+ type ChatContext<TRow extends DataEditorRow = DataEditorRow> = ChatDataContext<TRow> & {
2142
+ /** The user's chat prompt. */
2143
+ message: string;
2144
+ };
2145
+ /**
2146
+ * Bring-your-own-AI chat configuration. When provided via the `chat` prop,
2147
+ * the editor shows a chat panel alongside the grid. You own the AI
2148
+ * integration; the SDK hands you dataset context and renders the streamed
2149
+ * response.
2150
+ *
2151
+ * @example
2152
+ * ```ts
2153
+ * chat={{
2154
+ * sampleSize: 50,
2155
+ * onMessage: async function* (context) {
2156
+ * yield { type: "status", content: "Thinking..." };
2157
+ * const res = await fetch("/api/ai", {
2158
+ * method: "POST",
2159
+ * body: JSON.stringify({
2160
+ * prompt: context.message,
2161
+ * columns: context.columns,
2162
+ * sample: context.sample.map(r => r.data),
2163
+ * errors: context.errorSummary,
2164
+ * }),
2165
+ * }).then(r => r.json());
2166
+ * yield { type: "rows", content: res.updatedRows };
2167
+ * yield { type: "ops", content: res.ops };
2168
+ * yield { type: "message", content: res.reply };
2169
+ * },
2170
+ * }}
2171
+ * ```
2172
+ */
2173
+ type DataEditorChat<TRow extends DataEditorRow = DataEditorRow> = {
2174
+ /**
2175
+ * How many rows to include in the context sample. The SDK picks a
2176
+ * representative slice including rows with errors. When omitted, the SDK decides.
2177
+ */
2178
+ sampleSize?: number;
2179
+ /** Title shown above the suggestion list when the chat is empty. */
2180
+ emptyTitle?: string;
2181
+ /** How many suggestions to request from `loadSuggestions`. */
2182
+ suggestionsCount?: number;
2183
+ /** Optional prompt generator for the empty-state suggestion chips. */
2184
+ loadSuggestions?: (context: ChatDataContext<TRow>) => Promise<string[]>;
2185
+ /**
2186
+ * Called when the user sends a message. Receives full dataset context.
2187
+ * Returns an async iterable of response chunks — the SDK streams them
2188
+ * into the UI.
2189
+ */
2190
+ onMessage: (context: ChatContext<TRow>) => AsyncIterable<ChatResponseChunk<TRow>>;
2191
+ /** Called when the user cancels a pending request. Use to abort your API call. */
2192
+ onCancel?: () => void;
2193
+ };
2194
+
2195
+ type ApplyFormulaOptions = {
2196
+ /**
2197
+ * Column IDs to delete AFTER the formula has been applied.
2198
+ * Only dynamic (user-added) columns are actually deleted; schema columns
2199
+ * in this list are silently ignored. All operations (formula + deletes)
2200
+ * are wrapped in a single CompoundCommand so that one Undo call restores
2201
+ * every change, regardless of how many columns were listed.
2202
+ */
2203
+ readonly deleteColumnsAfter?: readonly string[];
2204
+ };
2205
+ declare class DataStore<TRow extends DataEditorRow = DataEditorRow> {
2206
+ private readonly _mode;
2207
+ isServer(): boolean;
2208
+ isClient(): boolean;
2209
+ private rowStore;
2210
+ readonly formulaRegistry: FormulaRegistry;
2211
+ private _isLoading;
2212
+ private sourceManager;
2213
+ readonly sourceLifecycle: SourceLifecycle<TRow>;
2214
+ private dirtyTracker;
2215
+ private filterEngine;
2216
+ private validationStore;
2217
+ private snapshotManager;
2218
+ private history;
2219
+ private validator;
2220
+ private valueIndex;
2221
+ private serverCounts;
2222
+ private editBuilder;
2223
+ private isUndoRedoing;
2224
+ private pendingBatchCommands;
2225
+ private _version;
2226
+ private _skipNotify;
2227
+ private _bulkMode;
2228
+ private _editedCells;
2229
+ private _primaryKey;
2230
+ /** Columns that are currently locked (pre-locked from schema + user-locked at runtime). */
2231
+ private _lockedColumns;
2232
+ /** Columns locked via schema definition — user cannot unlock these. */
2233
+ private _preLockedColumns;
2234
+ /** Last-known visible row range reported by the canvas scroll handler. */
2235
+ private _viewportStart;
2236
+ private _viewportEnd;
2237
+ private pendingMutations;
2238
+ private editParamsHistory;
2239
+ readonly errorHandler: ErrorHandler;
2240
+ readonly server: ServerDataManager<TRow> | null;
2241
+ private readonly strategy;
2242
+ private readonly serverStrategy;
2243
+ private snapshotReader;
1616
2244
  private flagReader;
1617
2245
  private buildErrorBitmask;
1618
2246
  private buildEditedBitmask;
@@ -1627,7 +2255,7 @@ declare class DataStore<TRow extends DataEditorRow = DataEditorRow> {
1627
2255
  private bulkMutationHost;
1628
2256
  private orchestrator;
1629
2257
  constructor(mode?: StoreMode, serverInit?: {
1630
- config: DataEditorServer<TRow>;
2258
+ config: ScaleClientApi<TRow>;
1631
2259
  sourceLabel: string;
1632
2260
  }, errorHandler?: ErrorHandler);
1633
2261
  get mode(): StoreMode;
@@ -1803,726 +2431,164 @@ declare class DataStore<TRow extends DataEditorRow = DataEditorRow> {
1803
2431
  * from `meta.changes` so that `isCellDirty()` and `getOriginalCellValue()`
1804
2432
  * work through the existing DirtyTracker comparison logic.
1805
2433
  */
1806
- private hydrateServerMeta;
1807
- private hydrateRowMeta;
1808
- applyServerRowMeta(serverRows: ServerRow<TRow>[], counts?: ServerQueryCounts): void;
1809
- clear(): void;
1810
- destroy(): void;
1811
- setFilters(filters: Partial<Filters>): void;
1812
- getFilters(): Filters;
1813
- setSort(sortState: SortState, sortType?: SortType, locales?: string[]): Promise<void>;
1814
- handleServerScroll(visibleStart: number, visibleEnd: number): void;
1815
- reloadServerData(): void;
1816
- resetFilters(): void;
1817
- fetchFilterOptions(): void;
1818
- getFilterOptions(): FilterOptionsResponse | null;
1819
- get hasServerExport(): boolean;
1820
- serverExport(format: DataEditorFormat, allRows: boolean, rtl: boolean): Promise<void>;
1821
- syncWorkerFlags(): void;
1822
- setCellValidation(rowId: TRowId, field: string, result: ValidationResult): void;
1823
- getCellValidation(rowId: TRowId, field: string): ValidationResult;
1824
- private clearRowValidations;
1825
- updateRow(rowId: TRowId, field: string, value: unknown): void;
1826
- updateRowDirect(rowId: TRowId, field: string, value: unknown): void;
1827
- /**
1828
- * Bulk-write a single column for many rows in one pass.
1829
- * Skips per-row validation, notification, and snapshot adjustments.
1830
- * Caller must handle validation (via Validator.revalidateColumn) and notification.
1831
- */
1832
- updateColumnDirect(field: string, values: ReadonlyMap<TRowId, unknown>): void;
1833
- batch(fn: () => void): number | undefined;
1834
- canUndo(): boolean;
1835
- getAllRowIds(): readonly TRowId[];
1836
- undo(): Promise<UndoRedoResult>;
1837
- private _undoSync;
1838
- pushCommand(cmd: Command<TRow>, cost?: number): number;
1839
- removeCommandById(id: number): void;
1840
- /**
1841
- * Delegate to ServerStrategy. Called by ActionsDispatcher and ClipboardManager
1842
- * for non-cell edits (clear, paste, fill, transform).
1843
- */
1844
- syncServerEdit(params: EditParams, cmdId: number, revertFn: () => void): void;
1845
- fireServerEditParams(params: EditParams): void;
1846
- syncColumnEdit(params: ColumnEditParams, cmdId: number, revertFn: () => void): void;
1847
- redo(): Promise<UndoRedoResult>;
1848
- private _redoSync;
1849
- getOriginalCellValue(rowId: TRowId, field: string): unknown | undefined;
1850
- isCellDirty(rowId: TRowId, field: string): boolean;
1851
- hasRowErrors(rowId: TRowId): boolean;
1852
- hasEmptyCells(rowId: TRowId): boolean;
1853
- private checkRowEmptyCells;
1854
- getValidAndInvalidRows(): {
1855
- valid: TRow[];
1856
- invalid: TRow[];
1857
- };
1858
- getValidRows(): TRow[];
1859
- getInvalidRows(): TRow[];
1860
- getResultBySource(): DataEditorResult<TRow>;
1861
- private isRowVisible;
1862
- applyFormula(formulaOrName: string | CellFormula, params: Record<string, unknown>, rects: SelectionRect[], options?: ApplyFormulaOptions): Promise<void>;
1863
- private applyFormulaCompound;
1864
- private runFormulaWithReturnCommand;
1865
- private _runFormulaOperation;
1866
- private captureDeleteColumnSnapshots;
1867
- private applyDeleteColumnSnapshots;
1868
- private syncFormulaToServer;
1869
- private transformWorker;
1870
- private initTransformWorker;
1871
- private buildChatOpsCommand;
1872
- applyChatOps(ops: ChatOp[], ctx: {
1873
- opts: Record<string, Set<string>>;
1874
- }, enableDeleteRow: 'all' | 'new' | false): Promise<void>;
1875
- private filterDeleteIdsByPolicy;
1876
- private _applyChatOpsViaWorker;
1877
- private _applyChatOpsSync;
1878
- private _commitChatOps;
1879
- applyChatRows(incomingRows: Record<string, unknown>[], primaryKey: string): Promise<void>;
1880
- private syncChatTransformToServer;
1881
- private get revertRowReader();
1882
- revertColumns(fields: string[]): Promise<void>;
1883
- revertRange(rects: SelectionRect[]): Promise<void>;
1884
- private _revertInternal;
1885
- private _buildRevertCommand;
1886
- private syncRevertToServer;
1887
- clearColumn(field: string): Promise<void>;
1888
- clearColumns(fields: string[]): Promise<void>;
1889
- private syncClearToServer;
1890
- deleteColumn(columnId: string): Promise<void>;
1891
- deleteColumns(columnIds: readonly string[]): Promise<void>;
1892
- clearRange(rects: SelectionRect[]): Promise<void>;
1893
- private syncRangeClearToServer;
1894
- pasteChunked(spec: PasteSpec, targetRowIds: TRowId[], targetCell: {
1895
- rowId: TRowId;
1896
- field: string;
1897
- }, onComplete?: (cmdId: number) => void): void;
1898
- /**
1899
- * Store-owned paste: cost-gated sync path or chunked orchestrator.
1900
- * Replaces executePasteWithOverlay + pasteChunked for internal-clipboard paste.
1901
- */
1902
- pasteInternal(spec: PasteSpec, targetRowIds: TRowId[], targetCell: {
1903
- rowId: TRowId;
1904
- field: string;
1905
- }, validator?: IValidator<TRow>): Promise<{
1906
- cmdId: number;
1907
- } | null>;
1908
- /**
1909
- * Store-owned external paste from TSV text (OS clipboard).
1910
- * Replaces the pasteFromText helper in pasteUtils.
1911
- */
1912
- pasteFromText(text: string, pasteRow: number, pasteCol: number, validator?: IValidator<TRow>): Promise<{
1913
- cmdId: number;
1914
- } | null>;
1915
- /**
1916
- * Store-owned fill: cost-gated sync path or chunked orchestrator.
1917
- * Replaces fillChunked + the per-caller isHeavy heuristic in ActionsDispatcher.
1918
- */
1919
- fillInternal(spec: FillSpec, targetRowIds: TRowId[], targetCell: {
1920
- rowId: TRowId;
1921
- field: string;
1922
- }, onComplete?: (cmdId: number) => void): Promise<{
1923
- cmdId: number;
1924
- } | null>;
1925
- notify(): void;
1926
- }
1927
-
1928
- /**
1929
- * Command interface for undo/redo operations.
1930
- * Each command knows how to apply and revert its changes.
1931
- */
1932
- interface Command<TRow extends DataEditorRow = DataEditorRow> {
1933
- /** Assigned by CommandHistory.push(). Used by removeById() to target specific commands. */
1934
- id?: number;
1935
- redo(store: DataStore<TRow>, validator: IValidator<TRow>): void;
1936
- undo(store: DataStore<TRow>, validator: IValidator<TRow>): void;
1937
- readonly description: string;
1938
- /** Target cell for selection restoration on undo/redo */
1939
- readonly targetCell: CellLocation;
1940
- }
1941
-
1942
- /**
1943
- * BulkMutationOrchestrator — Viewport-first chunked execution for heavy
1944
- * column operations (split, merge, clear, paste, fill, backspace).
1945
- *
1946
- * When operation cost >= LARGE_OP_CELLS:
1947
- * 1. Viewport rows are applied synchronously (instant visual result)
1948
- * 2. Remaining rows are processed in chunks via requestIdleCallback
1949
- * 3. Validation runs chunked after all mutations complete
1950
- * 4. Command is pushed to history only after everything finishes
1951
- *
1952
- * Communicates with DataStore via BulkMutationHost interface to avoid
1953
- * circular imports. DataStore constructs the host from its own methods.
1954
- */
1955
-
1956
- type ColumnDelta = {
1957
- field: string;
1958
- oldValues: Map<TRowId, unknown>;
1959
- newValues: Map<TRowId, unknown>;
1960
- };
1961
-
1962
- /** Severity level for a validation message. */
1963
- type ValidationLevel = "error";
1964
- /**
1965
- * A single validation message attached to a cell.
1966
- * Return this from a `CellValidator` to flag a problem.
1967
- */
1968
- type ValidationError = {
1969
- level: ValidationLevel;
1970
- /** Human-readable message shown in the cell tooltip. */
1971
- message: string;
1972
- };
1973
- type ValidationResult = ValidationError[] | null;
1974
- /**
1975
- * A function that validates a single cell value.
1976
- * Return a `ValidationError` to flag a problem, or `null` if the value is valid.
1977
- *
1978
- * A `ValidationError` with `level: "error"` flags the cell in the grid but
1979
- * does not block submission — invalid rows are delivered to `onComplete`
1980
- * alongside valid ones, tagged via the `isValid` flag.
1981
- *
1982
- * Built-in validator factories: `required(msg)`, `numeric(msg)`, `email(msg)`,
1983
- * `date(msg)`, `oneOf(values, msg)`, `endDateAfterStart(startField, msg)`.
1984
- * Import them from the package root.
1985
- *
1986
- * @param value - The current cell value.
1987
- * @param row - The full row, useful for cross-field checks.
1988
- */
1989
- type CellValidator = (value: unknown, row: DataEditorRow) => ValidationError | null;
1990
- /** Text input cell. This is the default editor when no `editor` is specified. */
1991
- type TextEditorCell = {
1992
- type: "text";
1993
- };
1994
- /** Date picker cell. Optionally restrict the selectable date range. */
1995
- type DateEditorCell = {
1996
- type: "date";
1997
- /** Earliest selectable date. */
1998
- minDate?: Date;
1999
- /** Latest selectable date. */
2000
- maxDate?: Date;
2001
- };
2002
- /** Dropdown select cell. The user picks from a fixed list of options. */
2003
- type SelectEditorCell = {
2004
- type: "select";
2005
- /** The list of options shown in the dropdown. Each string is both the stored value and the display label. */
2006
- options: string[];
2007
- };
2008
- /** Number input cell with locale-aware formatting. */
2009
- type NumberEditorCell = {
2010
- type: "number";
2011
- /** Maximum number of decimal digits allowed. When omitted, decimals are unrestricted. */
2012
- decimalPlaces?: number;
2013
- /** Character used as the decimal point (e.g. `"."` or `","`). Defaults to the browser locale. */
2014
- decimalSeparator?: string;
2015
- /** Character inserted between groups of three digits (e.g. `","` or `"."`). Defaults to the browser locale. */
2016
- thousandsSeparator?: string;
2017
- /** Extra characters to allow beyond digits, decimal separator, and minus sign (e.g. `"%-"`). When defined, minus is only kept if explicitly included. */
2018
- allowChars?: string;
2019
- };
2020
- /**
2021
- * Controls how a cell is edited.
2022
- *
2023
- * - `"text"` — plain text input (default).
2024
- * - `"date"` — date picker with optional min/max bounds.
2025
- * - `"select"` — dropdown with a fixed list of options.
2026
- * - `"number"` — number input with locale-aware formatting.
2027
- */
2028
- type CellEditor = TextEditorCell | DateEditorCell | SelectEditorCell | NumberEditorCell;
2029
- /** Dropdown filter shown in the sidebar Filters panel. */
2030
- type SelectColumnFilter = {
2031
- type: "select";
2032
- /** Label displayed above the filter. */
2033
- label?: string;
2034
- /** Placeholder text when nothing is selected. */
2035
- placeholder?: string;
2036
- /** Fixed list of filter options. When omitted, options are derived from column values. */
2037
- options?: string[];
2038
- /** Allow selecting multiple values at once. */
2039
- multiple?: boolean;
2040
- };
2041
- /** Numeric min/max range filter shown in the sidebar Filters panel. */
2042
- type NumberRangeColumnFilter = {
2043
- type: "number-range";
2044
- /** Label displayed above the filter. */
2045
- label?: string;
2046
- };
2047
- /** Date min/max range filter shown in the sidebar Filters panel. */
2048
- type DateRangeColumnFilter = {
2049
- type: "date-range";
2050
- /** Label displayed above the filter. */
2051
- label?: string;
2052
- };
2053
- /**
2054
- * Filter control shown in the sidebar Filters panel for a column.
2055
- *
2056
- * - `"select"` — dropdown to pick one or more values.
2057
- * - `"number-range"` — two inputs for min and max number.
2058
- * - `"date-range"` — two date pickers for start and end date.
2059
- */
2060
- type ColumnFilter = SelectColumnFilter | NumberRangeColumnFilter | DateRangeColumnFilter;
2061
- /** Lock mode for a column. `"all"` locks for every row; `"default"` locks only default-source rows. */
2062
- type ColumnLockMode = "all" | "default";
2063
- /**
2064
- * Defines a column in the editor grid.
2065
- *
2066
- * @example
2067
- * ```ts
2068
- * import { required, email, numeric } from "@updog/data-editor";
2069
- *
2070
- * const columns: DataEditorColumn[] = [
2071
- * { id: "name", title: "Full Name", size: 200, validate: required("Name is required") },
2072
- * { id: "email", title: "Email", size: 250, validate: [required("Email is required"), email("Invalid email")], unique: true },
2073
- * { id: "role", title: "Role", editor: { type: "select", options: ["Admin", "Editor", "Viewer"] } },
2074
- * { id: "salary", title: "Salary", validate: numeric("Must be a number"), formatter: (v) => v ? `$${v}` : "" },
2075
- * ];
2076
- * ```
2077
- */
2078
- type DataEditorColumn = {
2079
- /** Unique column identifier. Must match the keys in your row data. */
2080
- id: string;
2081
- /** Column header text shown to the user. */
2082
- title: string;
2083
- /**
2084
- * One or more validators run on every edit. Each receives the cell value and
2085
- * the full row, and returns a `ValidationError` to flag a problem or `null`
2086
- * if valid. Errors do not block submission — see `CellValidator`.
2087
- */
2088
- validate?: CellValidator | CellValidator[];
2089
- /**
2090
- * When `true`, the editor flags duplicate values in this column as errors.
2091
- * The error message is localized via the `translations` prop
2092
- * (`dataEditor.validation.valueMustBeUnique`).
2093
- */
2094
- unique?: boolean;
2095
- /**
2096
- * Column IDs to revalidate when this column changes. Use for cross-field
2097
- * rules like "end date must be after start date".
2098
- */
2099
- dependentFields?: string[];
2100
- /** Format the display value without changing stored data. E.g. add `$` prefix. */
2101
- formatter?: (value: string) => string;
2102
- /**
2103
- * Transform a value before it enters the store. Runs when rows are uploaded
2104
- * to the data editor.
2105
- */
2106
- transformer?: (value: unknown) => unknown;
2107
- /** How the cell is edited. Defaults to text input. */
2108
- editor?: CellEditor;
2109
- /** Adds a filter control for this column in the sidebar Filters panel. */
2110
- filter?: ColumnFilter;
2111
- /**
2112
- * Whether this column can be pinned to the left (right in RTL) via the
2113
- * header context menu. @default true
2114
- */
2115
- pinnable?: boolean;
2116
- /** Column width in pixels. @default 150 */
2117
- size?: number;
2118
- /**
2119
- * Controls whether cells in this column are locked. A column locked via
2120
- * configuration cannot be unlocked from the UI.
2121
- * - `true` | `"all"` — locked for every row.
2122
- * - `"default"` — locked only for default-source rows; rows added manually,
2123
- * duplicated, or imported remain editable.
2124
- * - `false` | `undefined` — not locked.
2125
- */
2126
- locked?: boolean | ColumnLockMode;
2127
- };
2128
-
2129
- /** Params passed to `findAndReplace.onFind` when the user types a search query. */
2130
- type FindParams = {
2131
- /** The search string. */
2132
- search: string;
2133
- /** When `true`, matching is case-sensitive. */
2134
- matchCase?: boolean;
2135
- /** When `true`, the entire cell value must equal the search string. */
2136
- matchEntireCell?: boolean;
2137
- /** Restrict search to these columns. `null` or omitted = all columns. */
2138
- columnIds?: string[] | null;
2139
- /** Current view filters so the server can scope matches to the active filter set. */
2140
- filters?: QueryFilters;
2141
- /** Current sort state so match ordering follows the visual row order. */
2142
- sort?: SortState;
2143
- };
2144
- /** A single match location returned by the server. */
2145
- type FindMatch = {
2146
- /** Row position in the current filtered+sorted view (for grid scrolling). */
2147
- rowIndex: number;
2148
- /** Column ID where the match occurs. */
2149
- columnId: string;
2150
- /** Character offset within the cell value (for inline highlight). */
2151
- startIndex: number;
2152
- /** 0-based position in the ordered match list (for counter display). */
2153
- matchIndex: number;
2154
- };
2155
- /** Response from `findAndReplace.onFind`. */
2156
- type FindResponse = {
2157
- /** Total number of matches across all rows. */
2158
- totalCount: number;
2159
- /** The first match. Omit when `totalCount` is 0. */
2160
- current?: FindMatch;
2161
- };
2162
- /** Params passed to `findAndReplace.onNavigate` when the user clicks prev/next. */
2163
- type FindNavigateParams = FindParams & {
2164
- /** Navigation direction. */
2165
- direction: "next" | "prev";
2166
- /** Current match position so the server knows where to navigate from. */
2167
- currentMatchIndex: number;
2168
- };
2169
- /** Params passed to `findAndReplace.onReplace`. */
2170
- type ReplaceParams = FindParams & {
2171
- /** The replacement text. */
2172
- replacement: string;
2173
- /** When `true`, replace all matches. When omitted or `false`, replace only `target`. */
2174
- all?: boolean;
2175
- /** The specific match to replace. Required when `all` is not `true`. */
2176
- target?: FindMatch;
2177
- };
2178
- /** Response from `findAndReplace.onReplace`. */
2179
- type ReplaceResponse = {
2180
- /** Remaining match count after replacement. */
2181
- totalCount: number;
2182
- /** Next match to navigate to after replacement. Omit when none left. */
2183
- current?: FindMatch;
2184
- };
2185
- /** Server-side find and replace configuration. */
2186
- type FindAndReplaceConfig = {
2187
- /** Called when the user types a search query. Returns total count and first match. */
2188
- onFind: (params: FindParams) => Promise<FindResponse>;
2189
- /** Called when the user clicks prev/next arrows. Returns the target match. */
2190
- onNavigate: (params: FindNavigateParams) => Promise<FindMatch>;
2191
- /** Called when the user clicks Replace or Replace All. */
2192
- onReplace: (params: ReplaceParams) => Promise<ReplaceResponse>;
2193
- };
2194
- type StoreMode = "client" | "server";
2195
- type ServerRowId = string | number;
2196
- /** Row-level status flags returned by the server. Drive row filters and sidebar counts. */
2197
- type ServerRowStatus = {
2198
- edited?: boolean;
2199
- new?: boolean;
2200
- deleted?: boolean;
2201
- hasErrors?: boolean;
2202
- hasEmptyCells?: boolean;
2203
- };
2204
- /** Server-reported change for a single cell. The current value lives in `fields`. */
2205
- type ServerCellChange = {
2206
- original: unknown;
2207
- };
2208
- /** Server-reported validation error for a single cell. */
2209
- type ServerCellError = {
2210
- message: string;
2211
- code?: number | string;
2212
- };
2213
- /** Per-row metadata returned by the server in server-delegated mode. */
2214
- type ServerRowMeta = {
2215
- /** Row-level status flags — drive row filters and sidebar counts. */
2216
- status?: ServerRowStatus;
2217
- /** Cell-level change tracking. Key = field name. */
2218
- changes?: Record<string, ServerCellChange>;
2219
- /** Cell-level validation errors. Key = field name. */
2220
- errors?: Record<string, ServerCellError[]>;
2221
- };
2222
- /** Per-source row count returned by the server. Drives the Data Sources sidebar. */
2223
- type ServerSourceCount = {
2224
- id: string;
2225
- name: string;
2226
- count: number;
2227
- };
2228
- /** Aggregate row counts returned alongside a query page. Drive sidebar indicators. */
2229
- type ServerQueryCounts = {
2230
- edited?: number;
2231
- new?: number;
2232
- deleted?: number;
2233
- errors?: number;
2234
- emptyCells?: number;
2235
- sources?: ServerSourceCount[];
2236
- };
2237
- type ServerRow<T extends DataEditorRow = DataEditorRow> = {
2238
- id: ServerRowId;
2239
- fields: T;
2240
- meta?: ServerRowMeta;
2241
- };
2242
- type ServerResponse<T> = {
2243
- data: T;
2244
- meta?: Record<string, unknown>;
2245
- };
2246
- type QueryFilters<F = Record<string, unknown>> = Partial<Filters> & F;
2247
- type QueryParams<F = Record<string, unknown>> = {
2248
- filters?: QueryFilters<F>;
2249
- sort?: SortState;
2250
- /** When present, only rows belonging to these source IDs are returned. Omit to include all sources. */
2251
- sources?: string[];
2252
- offset?: number;
2253
- limit: number;
2254
- signal?: AbortSignal;
2255
- };
2256
- type QueryResponse<T extends DataEditorRow = DataEditorRow> = ServerResponse<{
2257
- rows: ServerRow<T>[];
2258
- totalCount: number;
2259
- filteredCount?: number;
2260
- counts?: ServerQueryCounts;
2261
- }>;
2262
- /**
2263
- * Filter options returned by `onFilterOptions` for populating sidebar filter controls in server mode.
2264
- * Keys are column IDs. Only include columns that have a `filter` configured.
2265
- */
2266
- type FilterOptionsResponse = {
2267
- [columnId: string]: {
2268
- /** Values for `"select"` filters. Raw display strings — no formatter is applied. */
2269
- options?: string[];
2270
- /** Bounds for `"number-range"` filters. */
2271
- range?: {
2272
- min: number;
2273
- max: number;
2274
- };
2275
- /** Bounds for `"date-range"` filters. Values are ISO date strings (YYYY-MM-DD). */
2276
- dateRange?: {
2277
- min: string;
2278
- max: string;
2279
- };
2280
- };
2281
- };
2282
- type ServerCallOptions = {
2283
- signal: AbortSignal;
2284
- };
2285
- /**
2286
- * Coordinate rectangle within the server's data view.
2287
- * Uses ServerRowId (primary key) for rows and column ID strings for columns.
2288
- *
2289
- * Convention:
2290
- * - Both row fields omitted → all rows.
2291
- * - Both column fields omitted → all columns.
2292
- * - Single row: `fromRow` AND `toRow` both set, `toRow === fromRow`.
2293
- * - Single column: `fromColumn` AND `toColumn` both set, `toColumn === fromColumn`.
2294
- * - Range: both `from` and `to` set, `to !== from`.
2295
- * - `allSelected: true` → all rows and all columns.
2296
- * - Empty `{}` → used only for insert operations.
2297
- *
2298
- * INVALID: `from` present without `to`, or vice versa.
2299
- */
2300
- type Region = {
2301
- fromRow?: ServerRowId;
2302
- toRow?: ServerRowId;
2303
- fromColumn?: string;
2304
- toColumn?: string;
2305
- allSelected?: boolean;
2306
- };
2307
- /** Describes where and how to insert a new row. */
2308
- type InsertParams = {
2309
- /** Existing row to anchor the insert relative to. Omitted when appending to the end. */
2310
- anchorRow?: ServerRowId;
2311
- /** Insert before or after the anchor row. */
2312
- position: "above" | "below";
2313
- /** Column IDs matching the order of `values` entries. */
2314
- columns: string[];
2315
- };
2316
- /**
2317
- * Unified edit params for all data mutations in server-delegated mode.
2318
- *
2319
- * The combination of fields determines the operation:
2320
- * - `target` + `values` → cell edit, clear, or external paste
2321
- * - `target` + `source` → internal paste (+ `cut` for cut-paste)
2322
- * - `target` + `source` (fill) → fill handle
2323
- * - `target` + `transform` → transform / revert
2324
- * - `target` + `delete: true` → mark rows for deletion
2325
- * - `target` + `delete: false` → restore rows (unmark deletion)
2326
- * - `insert` + `values` → create a new row
2327
- *
2328
- * `values` is always a 2D array. For a single cell edit: `[["newValue"]]`.
2329
- * For clearing: `[[""]]`. The server interprets dimensions relative to `target`:
2330
- * a 1×1 `values` applied to a multi-cell target means "fill all cells with this value".
2331
- *
2332
- * `filters` and `sort` provide the view context so the server can resolve
2333
- * which rows fall between `fromRow` and `toRow` in the current view.
2334
- * Omitted for single-cell edits where `fromRow` is a direct row ID.
2335
- */
2336
- type EditParams = {
2337
- target: Region[];
2338
- source?: Region[];
2339
- values?: unknown[][];
2340
- transform?: TransformParams;
2341
- cut?: boolean;
2342
- delete?: boolean;
2343
- insert?: InsertParams;
2344
- undo?: boolean;
2345
- filters?: QueryFilters;
2346
- sort?: SortState;
2347
- lockedColumns?: Array<{
2348
- columnId: string;
2349
- mode: "all" | "default";
2350
- }>;
2351
- };
2352
- type ExportParams$1<F = Record<string, unknown>> = {
2353
- format: "csv" | "tsv" | "xlsx" | "json" | "xml";
2354
- allRows: boolean;
2355
- rtl?: boolean;
2356
- filters?: QueryFilters<F>;
2357
- sort?: SortState;
2358
- signal?: AbortSignal;
2359
- };
2360
- /** Transform operation descriptor. `type` identifies the operation, optional fields carry parameters. */
2361
- type TransformParams = {
2362
- type: string;
2363
- separator?: string;
2364
- /** When `true`, the server should delete the dynamic source columns after applying the transform. */
2365
- deleteSource?: boolean;
2366
- };
2367
- type FormulaParams = {
2368
- formula: string;
2369
- params: Record<string, unknown>;
2370
- };
2371
- /** Params passed to `onColumnDelete` when the user deletes a dynamic column. */
2372
- type ColumnDeleteParams = {
2373
- /** ID of the dynamic column to delete. */
2374
- columnId: string;
2375
- /** `true` when the server should reverse this operation (undo). Omitted on initial call and redo. */
2376
- undo?: boolean;
2377
- };
2378
- /** Params passed to `onColumnEdit` when the user renames a dynamic column. */
2379
- type ColumnEditParams = {
2380
- /** ID of the dynamic column being edited. */
2381
- columnId: string;
2382
- /** New title for the column. */
2383
- title: string;
2384
- /** `true` when the server should reverse this operation (undo). Omitted on initial call and redo. */
2385
- undo?: boolean;
2386
- };
2387
- /** Server's response after applying a mutation. */
2388
- type EditResponse = {
2389
- counts?: ServerQueryCounts;
2390
- /** Business-logic rejection. SDK reverts the optimistic update and shows `reason` in a toast. */
2391
- rejected?: boolean;
2392
- /** Why the server rejected. Shown to the user as-is. */
2393
- reason?: string;
2394
- /** The full row created by the server. Returned for insert operations. */
2395
- row?: ServerRow;
2396
- /** Updated column list. When present, the SDK replaces its columns with this list. */
2397
- columns?: Array<{
2398
- id: string;
2399
- title: string;
2400
- }>;
2401
- };
2402
- /**
2403
- * Every decision the user made during the import wizard, packed into one object.
2404
- * You get the raw file and the full mapping config. Parse it however you want —
2405
- * stream it, bulk-load it, hand it to a background job. Your call.
2406
- */
2407
- type ImportMappings = {
2408
- /** CSV header → column ID. Headers the user left unmatched are `undefined`. */
2409
- columnMapping: Record<string, string | undefined>;
2410
- /**
2411
- * Value substitutions for select columns.
2412
- * Outer key = column ID, inner key = imported value, inner value = target option.
2413
- * Only present when at least one select column was matched.
2414
- */
2415
- valueMapping: Record<string, Record<string, string | undefined>>;
2416
- /** Column ID used to match imported rows against existing data. Same value as `DataEditorProps.primaryKey`. */
2417
- primaryKey: string;
2418
- /** Sheet name the user selected. Only present for multi-sheet XLSX files. */
2419
- selectedSheet?: string;
2420
- /** Zero-based index of the row the SDK detected as the header row. */
2421
- headerRowIndex: number;
2422
- /** Number format detected from the file contents. Affects how `"1.234,56"` vs `"1,234.56"` is read. */
2423
- numberFormat: "EU" | "US";
2434
+ private hydrateServerMeta;
2435
+ private hydrateRowMeta;
2436
+ applyServerRowMeta(serverRows: ServerRow<TRow>[], counts?: ServerQueryCounts): void;
2437
+ clear(): void;
2438
+ destroy(): void;
2439
+ setFilters(filters: Partial<Filters>): void;
2440
+ getFilters(): Filters;
2441
+ setSort(sortState: SortState, sortType?: SortType, locales?: string[]): Promise<void>;
2442
+ handleServerScroll(visibleStart: number, visibleEnd: number): void;
2443
+ reloadServerData(): void;
2444
+ resetFilters(): void;
2445
+ fetchFilterOptions(): void;
2446
+ getFilterOptions(): FilterOptionsResponse | null;
2447
+ get hasServerExport(): boolean;
2448
+ serverExport(format: DataEditorFormat, allRows: boolean, rtl: boolean): Promise<void>;
2449
+ syncWorkerFlags(): void;
2450
+ setCellValidation(rowId: TRowId, field: string, result: ValidationResult): void;
2451
+ getCellValidation(rowId: TRowId, field: string): ValidationResult;
2452
+ private clearRowValidations;
2453
+ updateRow(rowId: TRowId, field: string, value: unknown): void;
2454
+ updateRowDirect(rowId: TRowId, field: string, value: unknown): void;
2424
2455
  /**
2425
- * Per-column date disambiguation. Key = CSV header (not column ID).
2426
- * `"EU"` = day-first, `"US"` = month-first.
2427
- * Only includes columns where the format was ambiguous and had to be resolved.
2456
+ * Bulk-write a single column for many rows in one pass.
2457
+ * Skips per-row validation, notification, and snapshot adjustments.
2458
+ * Caller must handle validation (via Validator.revalidateColumn) and notification.
2428
2459
  */
2429
- dateFormats: Record<string, "EU" | "US">;
2460
+ updateColumnDirect(field: string, values: ReadonlyMap<TRowId, unknown>): void;
2461
+ batch(fn: () => void): number | undefined;
2462
+ canUndo(): boolean;
2463
+ getAllRowIds(): readonly TRowId[];
2464
+ undo(): Promise<UndoRedoResult>;
2465
+ private _undoSync;
2466
+ pushCommand(cmd: Command<TRow>, cost?: number): number;
2467
+ removeCommandById(id: number): void;
2430
2468
  /**
2431
- * Columns the user created during import for unmatched headers.
2432
- * These don't exist in your schema yet — you decide whether to persist them.
2469
+ * Delegate to ServerStrategy. Called by ActionsDispatcher and ClipboardManager
2470
+ * for non-cell edits (clear, paste, fill, transform).
2433
2471
  */
2434
- newColumns: DataEditorColumn[];
2435
- };
2436
- /** Params passed to `onFileImport`. The original file, unchanged, plus everything the wizard collected. */
2437
- type FileImportParams = {
2438
- /** The original file. Same bytes the user dropped into the browser. */
2439
- file: File;
2440
- /** All mapping decisions from the import wizard. */
2441
- mappings: ImportMappings;
2442
- /** Display name for this import source. Typically the file name. */
2443
- sourceName: string;
2444
- /** Fires when the user cancels the import. */
2445
- signal?: AbortSignal;
2446
- };
2447
- /**
2448
- * Params passed to `onRowsImport` once per chunk.
2449
- * The SDK already parsed the file, applied column mappings, normalized dates
2450
- * and numbers, and resolved value mappings. You get clean, schema-conformant rows.
2451
- */
2452
- type RowsImportParams = {
2453
- /** Stable ID for this import session. Use it for idempotency or correlation. */
2454
- importId: string;
2455
- /** Display name for this import source. Typically the file name. */
2456
- sourceName: string;
2457
- /** Zero-based chunk index. Together with `importId`, uniquely identifies each chunk. */
2458
- chunkIndex: number;
2459
- /** `true` on the last chunk. Safe to finalize, run post-import hooks, or trigger validation. */
2460
- isLastChunk: boolean;
2461
- /** Transformed rows, keyed by column ID. Ready to store. */
2462
- rows: Record<string, unknown>[];
2463
- /** Columns the user created during import. Only present on the first chunk (`chunkIndex === 0`). */
2464
- newColumns?: DataEditorColumn[];
2465
- /** Column ID used to match imported rows against existing data. */
2466
- primaryKey: string;
2467
- /** Fires when the user cancels mid-import. Clean up any partial state. */
2468
- signal?: AbortSignal;
2469
- };
2470
- /** Your response after processing a chunk. */
2471
- type RowsImportResponse = {
2472
- /** How many rows you accepted from this chunk. Drives progress reporting. */
2473
- accepted: number;
2474
- /** Per-row errors. `rowIndex` is relative to the chunk (0-based). Surfaced in the UI after import. */
2475
- errors?: Array<{
2476
- rowIndex: number;
2477
- message: string;
2478
- }>;
2479
- };
2480
- /** Params passed to `onSourceRemove` when the user deletes a data source. */
2481
- type SourceRemoveParams = {
2482
- /** The source ID to remove. Matches the `id` from `ServerQueryCounts.sources`. */
2483
- sourceId: string;
2484
- /** Fires when the user cancels. */
2485
- signal?: AbortSignal;
2486
- };
2487
- type DataEditorServer<TRow extends DataEditorRow = DataEditorRow, TFilters = Record<string, unknown>> = {
2488
- onQuery: (params: QueryParams<TFilters>) => Promise<QueryResponse<TRow>>;
2489
- /** Fetches filter dictionaries for all filterable columns. Called once after license validation. */
2490
- onFilterOptions?: () => Promise<FilterOptionsResponse>;
2491
- /** Called when the user triggers an export. The consumer handles file generation and download. */
2492
- onExport?: (params: ExportParams$1<TFilters>) => Promise<void>;
2493
- /** Called after optimistic local apply. Return `{ rejected: true }` for business-logic errors; throw for infra failures. */
2494
- onEdit: (params: EditParams, options?: ServerCallOptions) => Promise<EditResponse | void>;
2472
+ syncServerEdit(params: EditParams, cmdId: number, revertFn: () => void): void;
2473
+ fireServerEditParams(params: EditParams): void;
2474
+ syncColumnEdit(params: ColumnEditParams, cmdId: number, revertFn: () => void): void;
2475
+ redo(): Promise<UndoRedoResult>;
2476
+ private _redoSync;
2477
+ getOriginalCellValue(rowId: TRowId, field: string): unknown | undefined;
2478
+ isCellDirty(rowId: TRowId, field: string): boolean;
2479
+ hasRowErrors(rowId: TRowId): boolean;
2480
+ hasEmptyCells(rowId: TRowId): boolean;
2481
+ private checkRowEmptyCells;
2482
+ getValidAndInvalidRows(): {
2483
+ valid: TRow[];
2484
+ invalid: TRow[];
2485
+ };
2486
+ getValidRows(): TRow[];
2487
+ getInvalidRows(): TRow[];
2488
+ getResultBySource(): DataEditorResult<TRow>;
2489
+ private isRowVisible;
2490
+ applyFormula(formulaOrName: string | CellFormula, params: Record<string, unknown>, rects: SelectionRect[], options?: ApplyFormulaOptions): Promise<void>;
2491
+ private applyFormulaCompound;
2492
+ private runFormulaWithReturnCommand;
2493
+ private _runFormulaOperation;
2494
+ private captureDeleteColumnSnapshots;
2495
+ private applyDeleteColumnSnapshots;
2496
+ private syncFormulaToServer;
2497
+ private transformWorker;
2498
+ private initTransformWorker;
2499
+ private buildChatOpsCommand;
2500
+ applyChatOps(ops: ChatOp[], ctx: {
2501
+ opts: Record<string, Set<string>>;
2502
+ }, enableDeleteRow: 'all' | 'new' | false): Promise<void>;
2503
+ private filterDeleteIdsByPolicy;
2504
+ private _applyChatOpsViaWorker;
2505
+ private _applyChatOpsSync;
2506
+ private _commitChatOps;
2507
+ applyChatRows(incomingRows: Record<string, unknown>[], primaryKey: string): Promise<void>;
2508
+ private syncChatTransformToServer;
2509
+ private get revertRowReader();
2510
+ revertColumns(fields: string[]): Promise<void>;
2511
+ revertRange(rects: SelectionRect[]): Promise<void>;
2512
+ private _revertInternal;
2513
+ private _buildRevertCommand;
2514
+ private syncRevertToServer;
2515
+ clearColumn(field: string): Promise<void>;
2516
+ clearColumns(fields: string[]): Promise<void>;
2517
+ private syncClearToServer;
2518
+ deleteColumn(columnId: string): Promise<void>;
2519
+ deleteColumns(columnIds: readonly string[]): Promise<void>;
2520
+ clearRange(rects: SelectionRect[]): Promise<void>;
2521
+ private syncRangeClearToServer;
2522
+ pasteChunked(spec: PasteSpec, targetRowIds: TRowId[], targetCell: {
2523
+ rowId: TRowId;
2524
+ field: string;
2525
+ }, onComplete?: (cmdId: number) => void): void;
2495
2526
  /**
2496
- * File import strategy. You get the raw file and the mapping config.
2497
- * You own parsing, transformation, and storage.
2498
- * Mutually exclusive with `onRowsImport`.
2527
+ * Store-owned paste: cost-gated sync path or chunked orchestrator.
2528
+ * Replaces executePasteWithOverlay + pasteChunked for internal-clipboard paste.
2499
2529
  */
2500
- onFileImport?: (params: FileImportParams) => Promise<void>;
2530
+ pasteInternal(spec: PasteSpec, targetRowIds: TRowId[], targetCell: {
2531
+ rowId: TRowId;
2532
+ field: string;
2533
+ }, validator?: IValidator<TRow>): Promise<{
2534
+ cmdId: number;
2535
+ } | null>;
2501
2536
  /**
2502
- * Rows import strategy. The SDK parses the file, applies all mappings,
2503
- * normalizes dates and numbers, and sends you clean rows in chunks.
2504
- * Called once per chunk. Awaited before sending the next chunk.
2505
- * Mutually exclusive with `onFileImport`.
2537
+ * Store-owned external paste from TSV text (OS clipboard).
2538
+ * Replaces the pasteFromText helper in pasteUtils.
2506
2539
  */
2507
- onRowsImport?: (params: RowsImportParams) => Promise<RowsImportResponse | void>;
2508
- /** Number of rows per import chunk when using `onRowsImport`. @default 5000 */
2509
- importChunkSize?: number;
2510
- /** Called when the user removes a data source. Delete all rows belonging to this source. */
2511
- onSourceRemove?: (params: SourceRemoveParams) => Promise<void>;
2512
- /** Called when the user deletes, undoes, or redoes a dynamic column deletion. */
2513
- onColumnDelete?: (params: ColumnDeleteParams) => Promise<EditResponse | void>;
2514
- /** Called when the user edits a dynamic column (e.g. renames it). */
2515
- onColumnEdit?: (params: ColumnEditParams) => Promise<EditResponse | void>;
2540
+ pasteFromText(text: string, pasteRow: number, pasteCol: number, validator?: IValidator<TRow>): Promise<{
2541
+ cmdId: number;
2542
+ } | null>;
2516
2543
  /**
2517
- * Server-side find and replace. When provided, enables Find & Replace UI
2518
- * in server mode. The server owns all searching and replacing; the SDK
2519
- * handles counts, navigation, and highlighting.
2544
+ * Store-owned fill: cost-gated sync path or chunked orchestrator.
2545
+ * Replaces fillChunked + the per-caller isHeavy heuristic in ActionsDispatcher.
2520
2546
  */
2521
- findAndReplace?: FindAndReplaceConfig;
2522
- /** Number of rows per page. @default 200 */
2523
- pageSize?: number;
2524
- /** Controls how eagerly the SDK fetches data while scrolling. 0 = always debounce, 1 = fetch immediately. @default 0.5 */
2525
- scrollSensitivity?: number;
2547
+ fillInternal(spec: FillSpec, targetRowIds: TRowId[], targetCell: {
2548
+ rowId: TRowId;
2549
+ field: string;
2550
+ }, onComplete?: (cmdId: number) => void): Promise<{
2551
+ cmdId: number;
2552
+ } | null>;
2553
+ notify(): void;
2554
+ }
2555
+
2556
+ /**
2557
+ * Command interface for undo/redo operations.
2558
+ * Each command knows how to apply and revert its changes.
2559
+ */
2560
+ interface Command<TRow extends DataEditorRow = DataEditorRow> {
2561
+ /** Assigned by CommandHistory.push(). Used by removeById() to target specific commands. */
2562
+ id?: number;
2563
+ redo(store: DataStore<TRow>, validator: IValidator<TRow>): void;
2564
+ undo(store: DataStore<TRow>, validator: IValidator<TRow>): void;
2565
+ readonly description: string;
2566
+ /** Target cell for selection restoration on undo/redo */
2567
+ readonly targetCell: CellLocation;
2568
+ }
2569
+
2570
+ /**
2571
+ * BulkMutationOrchestrator — Viewport-first chunked execution for heavy
2572
+ * column operations (split, merge, clear, paste, fill, backspace).
2573
+ *
2574
+ * When operation cost >= LARGE_OP_CELLS:
2575
+ * 1. Viewport rows are applied synchronously (instant visual result)
2576
+ * 2. Remaining rows are processed in chunks via requestIdleCallback
2577
+ * 3. Validation runs chunked after all mutations complete
2578
+ * 4. Command is pushed to history only after everything finishes
2579
+ *
2580
+ * Communicates with DataStore via BulkMutationHost interface to avoid
2581
+ * circular imports. DataStore constructs the host from its own methods.
2582
+ */
2583
+
2584
+ type ColumnDelta = {
2585
+ field: string;
2586
+ oldValues: Map<TRowId, unknown>;
2587
+ newValues: Map<TRowId, unknown>;
2588
+ };
2589
+
2590
+ type ScaleServerConfig = {
2591
+ url: string;
2526
2592
  };
2527
2593
 
2528
2594
  /** Numeric row identifier. V8 stores small integers (Smi) inline — no heap allocation. */
@@ -2779,12 +2845,6 @@ type DataEditorBaseProps<TRow extends DataEditorRow = DataEditorRow> = {
2779
2845
  * ```
2780
2846
  */
2781
2847
  loadData?: (onChunk: (rows: TRow[], options?: ChunkSourceOptions) => void) => Promise<void>;
2782
- /**
2783
- * Server-delegated mode. When provided, the SDK acts as a rendering head
2784
- * and delegates data operations (fetching, filtering, sorting, mutations)
2785
- * to the client's backend via callbacks.
2786
- */
2787
- server?: DataEditorServer<TRow>;
2788
2848
  /**
2789
2849
  * Called when the user clicks "Submit". Receives the edited data grouped
2790
2850
  * by source. Use `actions.reset()` to clear changes after a successful save.
@@ -2944,6 +3004,11 @@ type DataEditorMode = "modal" | "inline";
2944
3004
  type DataEditorCommonProps<TRow extends DataEditorRow = DataEditorRow> = DataEditorBaseProps<TRow> & {
2945
3005
  /** Your Updog license key. Validated on each open. */
2946
3006
  apiKey: string;
3007
+ /**
3008
+ * Connect to a self-hosted Updog Scale binary. When provided, the SDK
3009
+ * delegates all data operations to the Scale server at the given URL.
3010
+ */
3011
+ server?: ScaleServerConfig;
2947
3012
  /**
2948
3013
  * Controls what the editor stores in `localStorage`. Set to `false` to
2949
3014
  * disable all local storage usage.
@@ -3050,67 +3115,6 @@ declare function downloadExampleFile(columns: DataEditorColumn[], format: DataEd
3050
3115
  */
3051
3116
  declare function exportDataEditor<TRow extends DataEditorRow>(params: ExportParams<TRow>): Promise<void>;
3052
3117
 
3053
- /**
3054
- * Creates a validator that rejects empty values.
3055
- *
3056
- * @param message - Error message shown when the cell is empty.
3057
- *
3058
- * @example
3059
- * ```ts
3060
- * const columns = [
3061
- * { id: "name", title: "Name", validate: [required("Name is required")] },
3062
- * ];
3063
- * ```
3064
- */
3065
- declare function required(message: string): CellValidator;
3066
- /**
3067
- * Creates a validator that rejects non-numeric values.
3068
- *
3069
- * @param message - Error message shown when the value is not a valid number.
3070
- */
3071
- declare function numeric(message: string): CellValidator;
3072
- /**
3073
- * Creates a validator that rejects invalid email addresses.
3074
- *
3075
- * @param message - Error message shown when the value is not a valid email.
3076
- */
3077
- declare function email(message: string): CellValidator;
3078
- /**
3079
- * Creates a validator that rejects values not matching `YYYY-MM-DD` or `DD/MM/YYYY`.
3080
- *
3081
- * @param message - Error message shown when the value is not a valid date.
3082
- */
3083
- declare function date(message: string): CellValidator;
3084
- /**
3085
- * Creates a validator that ensures this date is on or after the date in another column.
3086
- *
3087
- * @param startDateField - Column ID of the start-date field to compare against.
3088
- * @param message - Error message shown when the end date is before the start date.
3089
- *
3090
- * @example
3091
- * ```ts
3092
- * const columns = [
3093
- * { id: "startDate", title: "Start", dependentFields: ["endDate"] },
3094
- * { id: "endDate", title: "End", validate: [endDateAfterStart("startDate", "End must be after start")] },
3095
- * ];
3096
- * ```
3097
- */
3098
- /**
3099
- * Creates a validator that rejects values not in the allowed set.
3100
- *
3101
- * @param values - Array of allowed values (matched by strict equality).
3102
- * @param message - Error message shown when the value is not in the set.
3103
- *
3104
- * @example
3105
- * ```ts
3106
- * const columns = [
3107
- * { id: "status", title: "Status", validate: [oneOf(["active", "inactive"], "Invalid status")] },
3108
- * ];
3109
- * ```
3110
- */
3111
- declare function oneOf(values: string[], message: string): CellValidator;
3112
- declare function endDateAfterStart(startDateField: string, message: string): CellValidator;
3113
-
3114
3118
  /**
3115
3119
  * Full-screen modal spreadsheet editor for large datasets.
3116
3120
  *
@@ -3127,6 +3131,7 @@ declare function endDateAfterStart(startDateField: string, message: string): Cel
3127
3131
  * onClose={() => setIsOpen(false)}
3128
3132
  * columns={columns}
3129
3133
  * primaryKey="id"
3134
+ * server={{ url: scaleUrl }}
3130
3135
  * loadData={async (onChunk) => onChunk(await fetchRows())}
3131
3136
  * onComplete={(result, actions) => { saveChanges(result); actions.reset(); }}
3132
3137
  * />
@@ -3134,5 +3139,5 @@ declare function endDateAfterStart(startDateField: string, message: string): Cel
3134
3139
  */
3135
3140
  declare function DataEditor<TRow extends DataEditorRow = DataEditorRow>(allProps: DataEditorProps<TRow>): react_jsx_runtime.JSX.Element;
3136
3141
 
3137
- export { DataEditor, date, downloadExampleFile, email, endDateAfterStart, exportDataEditor, numeric, oneOf, required };
3138
- export type { CellValidator, ChatContext, ChatErrorSummary, ChatResponseChunk, ChatRow, ChatRowStatus, ChunkSourceOptions, ColumnDeleteParams, ColumnEditParams, DataEditorChat, DataEditorColumn, DataEditorFormat, DataEditorInlineProps, DataEditorLocalStorage, DataEditorModalProps, DataEditorMode, DataEditorProps, DataEditorResult, DataEditorRow, DataEditorServer, DataEditorSourceResult, DataEditorTranslations, DataEditorVariant, EditParams, EditResponse, ExportParams$1 as ExportParams, FileImportParams, FilterOptionsResponse, FindNavigateParams, FindParams, FormulaParams, ImportMappings, QueryFilters, QueryParams, QueryResponse, Region, RemoteSource, ReplaceParams, ResultRow, RowsImportParams, RowsImportResponse, ServerCallOptions, ServerCellChange, ServerCellError, ServerQueryCounts, ServerRow, ServerRowMeta, ServerRowStatus, SourceRemoveParams, TransformParams, UpdogError, UpdogErrorCode, ValidationError, ValueMatchInput, ValueMatchOutput };
3142
+ export { DataEditor, downloadExampleFile, exportDataEditor };
3143
+ export type { CellValidator, ChatContext, ChatErrorSummary, ChatResponseChunk, ChatRow, ChatRowStatus, ChunkSourceOptions, DataEditorChat, DataEditorColumn, DataEditorFormat, DataEditorInlineProps, DataEditorLocalStorage, DataEditorModalProps, DataEditorMode, DataEditorProps, DataEditorResult, DataEditorRow, DataEditorSourceResult, DataEditorTranslations, DataEditorVariant, RemoteSource, ResultRow, ScaleServerConfig, UpdogError, UpdogErrorCode, ValidationError, ValueMatchInput, ValueMatchOutput };