@jxsuite/studio 0.37.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/dist/iframe-entry.js +622 -49
  2. package/dist/iframe-entry.js.map +15 -12
  3. package/dist/studio.css +1333 -0
  4. package/dist/studio.js +103709 -81001
  5. package/dist/studio.js.map +201 -157
  6. package/package.json +47 -45
  7. package/src/account-status.ts +39 -0
  8. package/src/browse/browse.ts +10 -14
  9. package/src/canvas/canvas-live-render.ts +21 -2
  10. package/src/canvas/canvas-render.ts +35 -16
  11. package/src/canvas/canvas-utils.ts +118 -72
  12. package/src/canvas/iframe-entry.ts +20 -0
  13. package/src/canvas/iframe-eval.ts +155 -0
  14. package/src/canvas/iframe-host.ts +132 -6
  15. package/src/canvas/iframe-inline-edit.ts +107 -1
  16. package/src/canvas/iframe-protocol.ts +43 -3
  17. package/src/canvas/iframe-render.ts +18 -0
  18. package/src/collab/collab-session.ts +23 -8
  19. package/src/collab/collab-state.ts +6 -3
  20. package/src/component-props.ts +104 -0
  21. package/src/editor/context-menu.ts +1 -1
  22. package/src/editor/inline-edit-apply.ts +36 -1
  23. package/src/editor/inline-edit.ts +58 -1
  24. package/src/editor/repeater-scope.ts +8 -13
  25. package/src/editor/shortcuts.ts +41 -12
  26. package/src/files/file-ops.ts +14 -0
  27. package/src/files/files.ts +56 -1
  28. package/src/format/format-host.ts +63 -1
  29. package/src/grid/cell-editors.ts +288 -0
  30. package/src/grid/cell-popovers.ts +108 -0
  31. package/src/grid/csv-codec.ts +122 -0
  32. package/src/grid/edit-buffer.ts +490 -0
  33. package/src/grid/grid-controller.ts +417 -0
  34. package/src/grid/grid-layout.ts +83 -0
  35. package/src/grid/grid-open.ts +167 -0
  36. package/src/grid/grid-panel.ts +302 -0
  37. package/src/grid/grid-source.ts +210 -0
  38. package/src/grid/grid-view.ts +367 -0
  39. package/src/grid/schema-columns.ts +261 -0
  40. package/src/grid/sources/connector-source.ts +217 -0
  41. package/src/grid/sources/content-source.ts +542 -0
  42. package/src/grid/sources/csv-file-source.ts +247 -0
  43. package/src/grid/tabulator-tables.d.ts +120 -0
  44. package/src/new-project/add-repo-modal.ts +183 -0
  45. package/src/new-project/new-project-modal.ts +22 -3
  46. package/src/page-params.ts +34 -8
  47. package/src/panels/ai-chat/chat-markdown.ts +2 -2
  48. package/src/panels/ai-panel.ts +61 -4
  49. package/src/panels/block-action-bar.ts +8 -3
  50. package/src/panels/chat-panel.ts +98 -0
  51. package/src/panels/data-grid.ts +241 -0
  52. package/src/panels/editors.ts +43 -17
  53. package/src/panels/events-panel.ts +137 -44
  54. package/src/panels/formula-workspace.ts +379 -0
  55. package/src/panels/frontmatter-fields.ts +231 -0
  56. package/src/panels/frontmatter-panel.ts +132 -0
  57. package/src/panels/git-panel.ts +1 -1
  58. package/src/panels/head-panel.ts +11 -175
  59. package/src/panels/properties-panel.ts +154 -144
  60. package/src/panels/right-panel.ts +9 -47
  61. package/src/panels/signals-panel.ts +217 -460
  62. package/src/panels/statement-editor.ts +710 -0
  63. package/src/panels/statusbar.ts +1 -1
  64. package/src/panels/style-panel.ts +25 -1
  65. package/src/panels/stylebook-panel.ts +0 -2
  66. package/src/panels/tab-bar.ts +175 -6
  67. package/src/panels/tab-strip.ts +62 -6
  68. package/src/panels/toolbar.ts +37 -3
  69. package/src/panels/welcome-screen.ts +50 -0
  70. package/src/platform-errors.ts +30 -0
  71. package/src/platforms/cloud.ts +172 -89
  72. package/src/platforms/devserver.ts +172 -0
  73. package/src/services/ai-project-tools.ts +541 -0
  74. package/src/services/ai-session-store.ts +28 -0
  75. package/src/services/ai-system-prompt.ts +194 -24
  76. package/src/services/ai-tools.ts +88 -21
  77. package/src/services/automation.ts +16 -0
  78. package/src/services/context-resolver.ts +73 -0
  79. package/src/services/data-service.ts +155 -0
  80. package/src/services/document-assistant.ts +81 -11
  81. package/src/services/gated-registry.ts +72 -0
  82. package/src/services/live-preview.ts +0 -0
  83. package/src/services/monaco-setup.ts +75 -26
  84. package/src/services/preview-eval.ts +68 -0
  85. package/src/services/project-adoption.ts +31 -0
  86. package/src/settings/contributed-section.ts +406 -0
  87. package/src/settings/extension-sections.ts +145 -0
  88. package/src/settings/schema-field-ui.ts +4 -2
  89. package/src/settings/settings-modal.ts +101 -42
  90. package/src/site-context.ts +12 -1
  91. package/src/store.ts +4 -0
  92. package/src/studio.ts +107 -52
  93. package/src/tabs/patch-ops.ts +25 -0
  94. package/src/tabs/tab.ts +39 -1
  95. package/src/tabs/transact.ts +61 -14
  96. package/src/types.ts +132 -1
  97. package/src/ui/dynamic-slot.ts +272 -0
  98. package/src/ui/expression-editor.ts +423 -125
  99. package/src/ui/field-row.ts +5 -0
  100. package/src/ui/form-controls.ts +322 -0
  101. package/src/ui/formula-catalog.ts +557 -0
  102. package/src/ui/formula-chips.ts +216 -0
  103. package/src/ui/formula-palette.ts +211 -0
  104. package/src/ui/layers.ts +40 -0
  105. package/src/ui/media-picker.ts +1 -1
  106. package/src/ui/panel-resize.ts +15 -1
  107. package/src/ui/progress-modal.ts +2 -2
  108. package/src/ui/schema-form.ts +524 -0
  109. package/src/utils/preview-format.ts +26 -0
  110. package/src/utils/studio-utils.ts +4 -3
  111. package/src/view.ts +4 -4
  112. package/src/workspace/workspace.ts +14 -0
  113. package/src/settings/content-types-editor.ts +0 -599
@@ -0,0 +1,490 @@
1
+ /**
2
+ * Engine-agnostic edit buffer for grid tabs — the batch-save model's core.
3
+ *
4
+ * Cell edits, row inserts, and row deletes accumulate here (nothing touches disk/API until the user
5
+ * saves); the grid engine only renders this state. Values equal to their baseline prune back to
6
+ * clean. Undo/redo is an inverse-op stack over the buffer and SURVIVES save: a commit re-baselines
7
+ * instead of clearing history, so a post-save undo simply makes cells dirty again. The one
8
+ * exception is structural history — ops referencing successfully committed row inserts/deletes are
9
+ * filtered out, because their temp keys/rows no longer exist.
10
+ *
11
+ * Baselines are resolved through `opts.resolveBaseline` at comparison time (not captured), which is
12
+ * what lets history outlive commits.
13
+ */
14
+ import { reactive } from "../reactivity";
15
+ import { cellValuesEqual } from "./grid-source";
16
+ import type { CommitResult, GridCellValue, GridEditBatch } from "./grid-source";
17
+
18
+ export type BufferOp =
19
+ | {
20
+ kind: "cell";
21
+ rowKey: string;
22
+ field: string;
23
+ before: GridCellValue;
24
+ after: GridCellValue;
25
+ at: number;
26
+ }
27
+ | { kind: "insert"; tempKey: string; cells: Record<string, GridCellValue> }
28
+ | { kind: "drop-insert"; tempKey: string; cells: Record<string, GridCellValue> }
29
+ | { kind: "delete"; rowKey: string }
30
+ | { kind: "group"; ops: BufferOp[] };
31
+
32
+ export type CellState = "clean" | "dirty" | "error" | "stale";
33
+
34
+ export type RowState = "clean" | "dirty" | "pending-insert" | "pending-delete" | "stale";
35
+
36
+ export interface EditBufferState {
37
+ /** Map of rowKey → field → pending value (existing rows only). */
38
+ pending: Map<string, Map<string, { value: GridCellValue }>>;
39
+ /** Map of tempKey → the new row's cells (inserted rows carry their edits directly). */
40
+ inserts: Map<string, Record<string, GridCellValue>>;
41
+ deletes: Set<string>;
42
+ /** Map of rowKey → field (or "*" for whole-row) → error message from the last commit. */
43
+ errors: Map<string, Map<string, string>>;
44
+ stale: Set<string>;
45
+ undoStack: BufferOp[];
46
+ redoStack: BufferOp[];
47
+ }
48
+
49
+ export interface EditBufferOptions {
50
+ /** Current committed value of a cell — consulted at comparison time, never captured. */
51
+ resolveBaseline: (rowKey: string, field: string) => GridCellValue;
52
+ /** Merge window for consecutive edits to the same cell (ms). */
53
+ coalesceMs?: number;
54
+ now?: () => number;
55
+ }
56
+
57
+ export interface EditBuffer {
58
+ state: EditBufferState;
59
+ setCell: (rowKey: string, field: string, value: GridCellValue) => void;
60
+ /** Add a pending new row; returns its temp key. */
61
+ insertRow: (cells?: Record<string, GridCellValue>) => string;
62
+ deleteRow: (rowKey: string) => void;
63
+ /** Run edits as one undo group (paste, fill, multi-delete). Groups may not nest. */
64
+ group: (fn: () => void) => void;
65
+ /** Imperative group boundaries for edit bursts that span event callbacks (engine paste). */
66
+ beginGroup: () => void;
67
+ endGroup: () => void;
68
+ undo: () => boolean;
69
+ redo: () => boolean;
70
+ canUndo: () => boolean;
71
+ canRedo: () => boolean;
72
+ isDirty: () => boolean;
73
+ /** Pending cells + inserts + deletes — the Save button badge. */
74
+ dirtyCount: () => number;
75
+ isInsertKey: (rowKey: string) => boolean;
76
+ effectiveValue: (rowKey: string, field: string) => GridCellValue;
77
+ cellState: (rowKey: string, field: string) => CellState;
78
+ rowState: (rowKey: string) => RowState;
79
+ cellError: (rowKey: string, field: string) => string | null;
80
+ buildBatch: () => GridEditBatch;
81
+ applyCommitResult: (result: CommitResult) => void;
82
+ markStale: (rowKeys: string[]) => void;
83
+ /** Drop one row's pending edits (and its history — a discard is not undoable). */
84
+ discardRow: (rowKey: string) => void;
85
+ reset: () => void;
86
+ }
87
+
88
+ let _tempCounter = 0;
89
+
90
+ /** Create the reactive edit buffer for one grid tab. */
91
+ export function createEditBuffer(opts: EditBufferOptions): EditBuffer {
92
+ const coalesceMs = opts.coalesceMs ?? 800;
93
+ const now = opts.now ?? (() => Date.now());
94
+
95
+ const state: EditBufferState = reactive({
96
+ deletes: new Set<string>(),
97
+ errors: new Map<string, Map<string, string>>(),
98
+ inserts: new Map<string, Record<string, GridCellValue>>(),
99
+ pending: new Map<string, Map<string, { value: GridCellValue }>>(),
100
+ redoStack: [] as BufferOp[],
101
+ stale: new Set<string>(),
102
+ undoStack: [] as BufferOp[],
103
+ }) as EditBufferState;
104
+
105
+ let groupOps: BufferOp[] | null = null;
106
+
107
+ // NB: on create paths, inner maps are built COMPLETE before insertion. Inserting an empty raw
108
+ // Map and then mutating the held reference writes past the reactive proxy — effects get
109
+ // Triggered at the empty intermediate state and never hear about the real content.
110
+ const setError = (rowKey: string, field: string, message: string) => {
111
+ const row = state.errors.get(rowKey);
112
+ if (row) {
113
+ row.set(field, message);
114
+ } else {
115
+ state.errors.set(rowKey, new Map([[field, message]]));
116
+ }
117
+ };
118
+
119
+ const clearCellError = (rowKey: string, field: string) => {
120
+ const row = state.errors.get(rowKey);
121
+ row?.delete(field);
122
+ if (row?.size === 0) {
123
+ state.errors.delete(rowKey);
124
+ }
125
+ };
126
+
127
+ const pushOp = (op: BufferOp) => {
128
+ state.redoStack.length = 0;
129
+ if (groupOps) {
130
+ groupOps.push(op);
131
+ return;
132
+ }
133
+ const last = state.undoStack.at(-1);
134
+ if (
135
+ op.kind === "cell" &&
136
+ last?.kind === "cell" &&
137
+ last.rowKey === op.rowKey &&
138
+ last.field === op.field &&
139
+ op.at - last.at <= coalesceMs
140
+ ) {
141
+ last.after = op.after;
142
+ last.at = op.at;
143
+ return;
144
+ }
145
+ state.undoStack.push(op);
146
+ };
147
+
148
+ /** Write a pending value without recording history (shared by user edits and undo/redo). */
149
+ const writeCell = (rowKey: string, field: string, value: GridCellValue) => {
150
+ const insertCells = state.inserts.get(rowKey);
151
+ if (insertCells) {
152
+ if (value === null) {
153
+ delete insertCells[field];
154
+ } else {
155
+ insertCells[field] = value;
156
+ }
157
+ return;
158
+ }
159
+ if (cellValuesEqual(value, opts.resolveBaseline(rowKey, field))) {
160
+ const row = state.pending.get(rowKey);
161
+ row?.delete(field);
162
+ if (row?.size === 0) {
163
+ state.pending.delete(rowKey);
164
+ }
165
+ return;
166
+ }
167
+ const row = state.pending.get(rowKey);
168
+ if (row) {
169
+ row.set(field, { value });
170
+ } else {
171
+ state.pending.set(rowKey, new Map([[field, { value }]]));
172
+ }
173
+ };
174
+
175
+ const effectiveValue = (rowKey: string, field: string): GridCellValue => {
176
+ const insertCells = state.inserts.get(rowKey);
177
+ if (insertCells) {
178
+ return insertCells[field] ?? null;
179
+ }
180
+ const pending = state.pending.get(rowKey)?.get(field);
181
+ return pending ? pending.value : opts.resolveBaseline(rowKey, field);
182
+ };
183
+
184
+ const cellError = (rowKey: string, field: string): string | null => {
185
+ const row = state.errors.get(rowKey);
186
+ return row?.get(field) ?? row?.get("*") ?? null;
187
+ };
188
+
189
+ const applyOp = (op: BufferOp, direction: "undo" | "redo") => {
190
+ switch (op.kind) {
191
+ case "cell": {
192
+ writeCell(op.rowKey, op.field, direction === "undo" ? op.before : op.after);
193
+ break;
194
+ }
195
+ case "insert": {
196
+ if (direction === "undo") {
197
+ state.inserts.delete(op.tempKey);
198
+ } else {
199
+ state.inserts.set(op.tempKey, { ...op.cells });
200
+ }
201
+ break;
202
+ }
203
+ case "drop-insert": {
204
+ if (direction === "undo") {
205
+ state.inserts.set(op.tempKey, { ...op.cells });
206
+ } else {
207
+ state.inserts.delete(op.tempKey);
208
+ }
209
+ break;
210
+ }
211
+ case "delete": {
212
+ if (direction === "undo") {
213
+ state.deletes.delete(op.rowKey);
214
+ } else {
215
+ state.deletes.add(op.rowKey);
216
+ }
217
+ break;
218
+ }
219
+ default: {
220
+ const ops = direction === "undo" ? op.ops.toReversed() : op.ops;
221
+ for (const child of ops) {
222
+ applyOp(child, direction);
223
+ }
224
+ break;
225
+ }
226
+ }
227
+ };
228
+
229
+ const beginGroup = () => {
230
+ if (groupOps) {
231
+ throw new Error("edit-buffer: groups may not nest");
232
+ }
233
+ groupOps = [];
234
+ };
235
+
236
+ const endGroup = () => {
237
+ const ops = groupOps;
238
+ groupOps = null;
239
+ if (!ops) {
240
+ return;
241
+ }
242
+ if (ops.length === 1) {
243
+ state.undoStack.push(ops[0]!);
244
+ } else if (ops.length > 1) {
245
+ state.undoStack.push({ kind: "group", ops });
246
+ }
247
+ };
248
+
249
+ /** Ops referencing a committed/discarded row can no longer replay — filter them out. */
250
+ const pruneHistory = (dropRow: (rowKey: string) => boolean) => {
251
+ const keep = (op: BufferOp): BufferOp | null => {
252
+ switch (op.kind) {
253
+ case "cell": {
254
+ return dropRow(op.rowKey) ? null : op;
255
+ }
256
+ case "insert":
257
+ case "drop-insert": {
258
+ return dropRow(op.tempKey) ? null : op;
259
+ }
260
+ case "delete": {
261
+ return dropRow(op.rowKey) ? null : op;
262
+ }
263
+ default: {
264
+ const ops = op.ops.map((child) => keep(child)).filter((o): o is BufferOp => o !== null);
265
+ return ops.length > 0 ? { kind: "group", ops } : null;
266
+ }
267
+ }
268
+ };
269
+ state.undoStack = state.undoStack.map(keep).filter((o): o is BufferOp => o !== null);
270
+ state.redoStack = state.redoStack.map(keep).filter((o): o is BufferOp => o !== null);
271
+ };
272
+
273
+ return {
274
+ applyCommitResult(result) {
275
+ const committedRows = new Set<string>();
276
+ for (const cell of result.cells) {
277
+ if (cell.ok) {
278
+ const row = state.pending.get(cell.rowKey);
279
+ row?.delete(cell.field);
280
+ if (row?.size === 0) {
281
+ state.pending.delete(cell.rowKey);
282
+ }
283
+ clearCellError(cell.rowKey, cell.field);
284
+ } else {
285
+ setError(cell.rowKey, cell.field, cell.error ?? "Save failed");
286
+ if (cell.stale) {
287
+ state.stale.add(cell.rowKey);
288
+ }
289
+ }
290
+ }
291
+ for (const ins of result.inserts) {
292
+ if (ins.ok) {
293
+ state.inserts.delete(ins.tempKey);
294
+ state.errors.delete(ins.tempKey);
295
+ committedRows.add(ins.tempKey);
296
+ } else {
297
+ setError(ins.tempKey, "*", ins.error ?? "Insert failed");
298
+ }
299
+ }
300
+ for (const del of result.deletes) {
301
+ if (del.ok) {
302
+ state.deletes.delete(del.rowKey);
303
+ state.pending.delete(del.rowKey);
304
+ state.errors.delete(del.rowKey);
305
+ committedRows.add(del.rowKey);
306
+ } else {
307
+ setError(del.rowKey, "*", del.error ?? "Delete failed");
308
+ if (del.stale) {
309
+ state.stale.add(del.rowKey);
310
+ }
311
+ }
312
+ }
313
+ for (const row of committedRows) {
314
+ state.stale.delete(row);
315
+ }
316
+ if (committedRows.size > 0) {
317
+ pruneHistory((rowKey) => committedRows.has(rowKey));
318
+ }
319
+ },
320
+
321
+ buildBatch(): GridEditBatch {
322
+ const cells: GridEditBatch["cells"] = [];
323
+ for (const [rowKey, fields] of state.pending) {
324
+ if (state.deletes.has(rowKey)) {
325
+ continue;
326
+ }
327
+ for (const [field, cell] of fields) {
328
+ cells.push({
329
+ baseline: opts.resolveBaseline(rowKey, field),
330
+ field,
331
+ rowKey,
332
+ value: cell.value,
333
+ });
334
+ }
335
+ }
336
+ return {
337
+ cells,
338
+ deletes: [...state.deletes].map((rowKey) => ({ rowKey })),
339
+ inserts: [...state.inserts].map(([tempKey, insertCells]) => ({
340
+ cells: { ...insertCells },
341
+ tempKey,
342
+ })),
343
+ };
344
+ },
345
+
346
+ canRedo: () => state.redoStack.length > 0,
347
+ canUndo: () => state.undoStack.length > 0,
348
+ cellError,
349
+
350
+ cellState(rowKey, field) {
351
+ if (cellError(rowKey, field) !== null) {
352
+ return "error";
353
+ }
354
+ if (state.stale.has(rowKey)) {
355
+ return "stale";
356
+ }
357
+ if (state.inserts.has(rowKey) || state.pending.get(rowKey)?.has(field)) {
358
+ return "dirty";
359
+ }
360
+ return "clean";
361
+ },
362
+
363
+ deleteRow(rowKey) {
364
+ if (state.deletes.has(rowKey)) {
365
+ return;
366
+ }
367
+ const insertCells = state.inserts.get(rowKey);
368
+ if (insertCells) {
369
+ // Deleting a not-yet-saved insert just drops it (the row disappears entirely).
370
+ const cells = { ...insertCells };
371
+ state.inserts.delete(rowKey);
372
+ state.errors.delete(rowKey);
373
+ state.stale.delete(rowKey);
374
+ pushOp({ cells, kind: "drop-insert", tempKey: rowKey });
375
+ return;
376
+ }
377
+ state.deletes.add(rowKey);
378
+ pushOp({ kind: "delete", rowKey });
379
+ },
380
+
381
+ dirtyCount() {
382
+ let count = state.inserts.size + state.deletes.size;
383
+ for (const [rowKey, fields] of state.pending) {
384
+ if (!state.deletes.has(rowKey)) {
385
+ count += fields.size;
386
+ }
387
+ }
388
+ return count;
389
+ },
390
+
391
+ discardRow(rowKey) {
392
+ state.pending.delete(rowKey);
393
+ state.deletes.delete(rowKey);
394
+ state.inserts.delete(rowKey);
395
+ state.errors.delete(rowKey);
396
+ state.stale.delete(rowKey);
397
+ pruneHistory((key) => key === rowKey);
398
+ },
399
+
400
+ effectiveValue,
401
+
402
+ beginGroup,
403
+
404
+ endGroup,
405
+
406
+ group(fn) {
407
+ beginGroup();
408
+ try {
409
+ fn();
410
+ } finally {
411
+ endGroup();
412
+ }
413
+ },
414
+
415
+ insertRow(cells = {}) {
416
+ _tempCounter += 1;
417
+ const tempKey = `__new-${_tempCounter}`;
418
+ state.inserts.set(tempKey, { ...cells });
419
+ pushOp({ cells: { ...cells }, kind: "insert", tempKey });
420
+ return tempKey;
421
+ },
422
+
423
+ isDirty: () => state.pending.size > 0 || state.inserts.size > 0 || state.deletes.size > 0,
424
+
425
+ isInsertKey: (rowKey) => state.inserts.has(rowKey),
426
+
427
+ markStale(rowKeys) {
428
+ for (const rowKey of rowKeys) {
429
+ state.stale.add(rowKey);
430
+ }
431
+ },
432
+
433
+ redo() {
434
+ const op = state.redoStack.pop();
435
+ if (!op) {
436
+ return false;
437
+ }
438
+ applyOp(op, "redo");
439
+ state.undoStack.push(op);
440
+ return true;
441
+ },
442
+
443
+ reset() {
444
+ state.pending.clear();
445
+ state.inserts.clear();
446
+ state.deletes.clear();
447
+ state.errors.clear();
448
+ state.stale.clear();
449
+ state.undoStack.length = 0;
450
+ state.redoStack.length = 0;
451
+ },
452
+
453
+ rowState(rowKey) {
454
+ if (state.deletes.has(rowKey)) {
455
+ return "pending-delete";
456
+ }
457
+ if (state.inserts.has(rowKey)) {
458
+ return "pending-insert";
459
+ }
460
+ if (state.stale.has(rowKey)) {
461
+ return "stale";
462
+ }
463
+ if (state.pending.has(rowKey) || state.errors.has(rowKey)) {
464
+ return "dirty";
465
+ }
466
+ return "clean";
467
+ },
468
+
469
+ setCell(rowKey, field, value) {
470
+ const before = effectiveValue(rowKey, field);
471
+ if (cellValuesEqual(before, value)) {
472
+ return;
473
+ }
474
+ writeCell(rowKey, field, value);
475
+ pushOp({ after: value, at: now(), before, field, kind: "cell", rowKey });
476
+ },
477
+
478
+ state,
479
+
480
+ undo() {
481
+ const op = state.undoStack.pop();
482
+ if (!op) {
483
+ return false;
484
+ }
485
+ applyOp(op, "undo");
486
+ state.redoStack.push(op);
487
+ return true;
488
+ },
489
+ };
490
+ }