@marimo-team/frontend 0.24.1-dev2 → 0.24.1-dev21

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 (136) hide show
  1. package/dist/assets/{CellStatus-DqJypoFY.js → CellStatus-DsvXjUsv.js} +1 -1
  2. package/dist/assets/{ConnectedDataExplorerComponent-DLvmqIcV.js → ConnectedDataExplorerComponent-WU7zjB7Z.js} +1 -1
  3. package/dist/assets/{JsonOutput-DnrBQBRT.js → JsonOutput-D62UH4Ip.js} +7 -7
  4. package/dist/assets/{MarimoErrorOutput-BVdFtnJz.js → MarimoErrorOutput-B_IfCm1k.js} +7 -7
  5. package/dist/assets/{RenderHTML-X9kskQZ0.js → RenderHTML-DY5kbf6q.js} +1 -1
  6. package/dist/assets/{RunButton-D83R2wvd.js → RunButton-BfHw11b3.js} +1 -1
  7. package/dist/assets/{add-cell-with-ai-HD16K3PB.js → add-cell-with-ai-B25FLoLF.js} +12 -12
  8. package/dist/assets/{add-connection-dialog-V86wrXca.js → add-connection-dialog-B05rbnef.js} +6 -6
  9. package/dist/assets/{agent-panel-XdLSrKjl.js → agent-panel-CQobsqAS.js} +6 -6
  10. package/dist/assets/{ai-model-dropdown-BJp70TST.js → ai-model-dropdown-CmDWOIkZ.js} +4 -4
  11. package/dist/assets/app-config-button-BkwiW00-.js +1 -0
  12. package/dist/assets/{blob-BKW9TqBu.js → blob-BqDSSN5X.js} +1 -1
  13. package/dist/assets/{cell-editor-DQSrfeL3.js → cell-editor-CefMlZnX.js} +14 -14
  14. package/dist/assets/cell-link-CP4zF1rO.js +1 -0
  15. package/dist/assets/{cells-DTFqEQb0.js → cells-DD-zwhxi.js} +72 -72
  16. package/dist/assets/{chat-display-CclXFQQP.js → chat-display-59QZ49m7.js} +1 -1
  17. package/dist/assets/{chat-panel-ft0VIKS8.js → chat-panel-CDZbG1OG.js} +2 -2
  18. package/dist/assets/{chat-ui-DUYenr6D.js → chat-ui-DoBTbTFY.js} +1 -1
  19. package/dist/assets/{column-preview-BLap3eAk.js → column-preview-BNg0hwmL.js} +1 -1
  20. package/dist/assets/{command-palette-C7nz0zHt.js → command-palette-gEcd355-.js} +1 -1
  21. package/dist/assets/{common-DKQZgzC9.js → common-CcYiDLMe.js} +1 -1
  22. package/dist/assets/{components-Bf_hykBb.js → components-B4D3PZ3j.js} +1 -1
  23. package/dist/assets/config-DFK5eCC3.js +1 -0
  24. package/dist/assets/{datasource-DZgUyqAK.js → datasource-DAM_bXu_.js} +2 -2
  25. package/dist/assets/dependency-graph-panel-01bZMvwS.js +5 -0
  26. package/dist/assets/{documentation-panel-BQYA8gCW.js → documentation-panel-BhruLk_O.js} +1 -1
  27. package/dist/assets/{download-_oAlkvj6.js → download-DT1x-ePG.js} +1 -1
  28. package/dist/assets/{edit-page-DnXTBsfC.js → edit-page-C8uQVdbi.js} +6 -6
  29. package/dist/assets/{error-entries-uxjSsTal.js → error-entries-Dg9m5P8l.js} +3 -3
  30. package/dist/assets/{error-panel-Bq0TKquh.js → error-panel-By_xNe1V.js} +1 -1
  31. package/dist/assets/{file-explorer-panel-uhg7VNMd.js → file-explorer-panel-BzTVzCS7.js} +3 -3
  32. package/dist/assets/{file-icons-V3puz20c.js → file-icons-Dop5RazI.js} +1 -1
  33. package/dist/assets/{file-name-input-DWf6YZwM.js → file-name-input-C5_fsJ6t.js} +1 -1
  34. package/dist/assets/{floating-outline-Vqr5D17R.js → floating-outline-Bt2aLttK.js} +1 -1
  35. package/dist/assets/{focus-BTlyH5K_.js → focus-DCZbWsqm.js} +1 -1
  36. package/dist/assets/{form-Bb1JVuzT.js → form-Cj3PfSUI.js} +1 -1
  37. package/dist/assets/{form-CRSnWxLR.js → form-DHy5wB3F.js} +1 -1
  38. package/dist/assets/{glide-data-editor-BNZxtmKn.js → glide-data-editor-BPPCsS2s.js} +2 -2
  39. package/dist/assets/{home-page-RduyhdYP.js → home-page-EsENjhWM.js} +2 -2
  40. package/dist/assets/{html-to-image-BsXiWo0v.js → html-to-image-CajdmeRM.js} +1 -1
  41. package/dist/assets/index-BZ-HxYIU.js +38 -0
  42. package/dist/assets/index-CxZ6uZaz.css +2 -0
  43. package/dist/assets/{kiosk-mode-BH15-Gn-.js → kiosk-mode-B8UhlOLw.js} +1 -1
  44. package/dist/assets/{layout-1X5SpE18.js → layout-B73d8K81.js} +5 -5
  45. package/dist/assets/{logs-panel-CVxksxou.js → logs-panel-CYMHvjs-.js} +1 -1
  46. package/dist/assets/{markdown-renderer-BjuEVUOa.js → markdown-renderer-DM-Whfio.js} +1 -1
  47. package/dist/assets/{name-cell-input-CtYGGTR5.js → name-cell-input-DF8MnZDV.js} +1 -1
  48. package/dist/assets/{outline-panel-BhxoKAbS.js → outline-panel-Q7h5hFfo.js} +1 -1
  49. package/dist/assets/{packages-panel-BWP3YWNW.js → packages-panel-BW4xGU0r.js} +1 -1
  50. package/dist/assets/{pair-with-agent-modal-BCMZbqSR.js → pair-with-agent-modal-DiDLAjxV.js} +1 -1
  51. package/dist/assets/panel-accordion-state-iff6EfKa.js +1 -0
  52. package/dist/assets/{panels-D40pa9F1.js → panels-BqKyRqOy.js} +2 -2
  53. package/dist/assets/{process-output-WaMx3G7b.js → process-output-B9grqmZC.js} +1 -1
  54. package/dist/assets/{radio-group-5KDs5aO1.js → radio-group-B8t5cIdU.js} +1 -1
  55. package/dist/assets/{readonly-code-display-DnHcjfFl.js → readonly-code-display-CY-jDwQL.js} +1 -1
  56. package/dist/assets/{readonly-python-code-BV8otm7e.js → readonly-python-code-DFjpK9Hc.js} +1 -1
  57. package/dist/assets/{reveal-component-yVci-fc9.js → reveal-component-DWjuDjk-.js} +2 -2
  58. package/dist/assets/{run-page-aQycuCjn.js → run-page-DNJz4YBq.js} +1 -1
  59. package/dist/assets/{scratchpad-panel-lsqN24Au.js → scratchpad-panel-BOQN7LjT.js} +1 -1
  60. package/dist/assets/{session-panel-C4JUkPt7.js → session-panel-C9zMMa1x.js} +1 -1
  61. package/dist/assets/{skeleton-DOZFvsSE.js → skeleton-mj_3hAv8.js} +1 -1
  62. package/dist/assets/{snippets-panel-QNY_Dl2J.js → snippets-panel-C-VlGXwd.js} +1 -1
  63. package/dist/assets/spec-C_psFYbu.js +1 -0
  64. package/dist/assets/state-BSVx1akM.js +3 -0
  65. package/dist/assets/{state-ByxLiTvP.js → state-C1e6in0R.js} +1 -1
  66. package/dist/assets/{switch-CDT2KH2g.js → switch-ulnbZFaI.js} +1 -1
  67. package/dist/assets/{terminal-UyHT9ie2.js → terminal-Bo6pnmSU.js} +1 -1
  68. package/dist/assets/{tracing-BiBPyXVg.js → tracing-D-L-9q2d.js} +1 -1
  69. package/dist/assets/{tracing-panel-D8LtcU_g.js → tracing-panel-DQzhu-hj.js} +2 -2
  70. package/dist/assets/{tree-actions-DUAdNn1i.js → tree-actions-BBmIijvt.js} +1 -1
  71. package/dist/assets/{useBoolean-CgN_4RAl.js → useBoolean-C_UV6tQ7.js} +1 -1
  72. package/dist/assets/{useCellActionButton-Bp2eEIzO.js → useCellActionButton-COoOWq__.js} +1 -1
  73. package/dist/assets/{useDeleteCell-DO-9USfB.js → useDeleteCell-C802AuEJ.js} +1 -1
  74. package/dist/assets/{useDependencyPanelTab-Bi8pUBnn.js → useDependencyPanelTab-DR6NOy5O.js} +1 -1
  75. package/dist/assets/useNotebookActions-BXVkcfqk.js +12 -0
  76. package/dist/assets/{useRunCells-BpZ_0Xuz.js → useRunCells-DuxMdaXo.js} +1 -1
  77. package/dist/assets/{useSplitCell-C2fjcfID.js → useSplitCell-B1EAP5Nv.js} +1 -1
  78. package/dist/assets/{utils-Y4n-X10o.js → utils-Bp3MLwWq.js} +1 -1
  79. package/dist/assets/{vega-component-3G_aA1Wa.js → vega-component-dzk5T-cc.js} +1 -1
  80. package/dist/assets/{vega-embed-container-CH1lX0V6.js → vega-embed-container-D0V0wn9T.js} +1 -1
  81. package/dist/index.html +29 -29
  82. package/package.json +1 -1
  83. package/src/__mocks__/requests.ts +4 -0
  84. package/src/components/app-config/optional-features.test.ts +78 -0
  85. package/src/components/app-config/optional-features.tsx +33 -6
  86. package/src/components/data-table/__tests__/column_formatting.test.ts +7 -0
  87. package/src/components/data-table/charts/__tests__/schemas.test.ts +17 -2
  88. package/src/components/data-table/charts/chart-spec/types.ts +2 -0
  89. package/src/components/data-table/column-formatting/feature.ts +1 -0
  90. package/src/components/data-table/column-formatting/types.ts +1 -0
  91. package/src/components/data-table/column-summary/chart-spec-model.tsx +1 -0
  92. package/src/components/data-table/column-summary/column-summary.tsx +1 -0
  93. package/src/components/data-table/columns.tsx +4 -3
  94. package/src/components/datasets/__tests__/icons.test.ts +10 -2
  95. package/src/components/datasets/icons.tsx +4 -0
  96. package/src/components/datasources/__tests__/components.test.tsx +14 -1
  97. package/src/components/editor/actions/export-dialog/__tests__/export-dialog.test.tsx +206 -34
  98. package/src/components/editor/actions/export-dialog/export-dialog.tsx +9 -3
  99. package/src/components/editor/actions/export-dialog/format-notice.tsx +69 -24
  100. package/src/components/editor/actions/export-dialog/use-export-dialog.ts +27 -2
  101. package/src/components/ui/toaster.tsx +14 -1
  102. package/src/core/codemirror/code-lens/__tests__/popover.test.ts +13 -0
  103. package/src/core/codemirror/code-lens/popover.tsx +11 -5
  104. package/src/core/codemirror/language/languages/sql/__tests__/renderers.test.tsx +15 -4
  105. package/src/core/codemirror/language/languages/sql/renderers.tsx +1 -0
  106. package/src/core/islands/bridge.ts +1 -0
  107. package/src/core/kernel/messages.ts +1 -0
  108. package/src/core/mode.ts +6 -1
  109. package/src/core/network/__tests__/requests-lazy.test.ts +40 -0
  110. package/src/core/network/__tests__/requests-network.test.ts +13 -0
  111. package/src/core/network/requests-lazy.ts +11 -4
  112. package/src/core/network/requests-network.ts +8 -0
  113. package/src/core/network/requests-static.ts +1 -0
  114. package/src/core/network/requests-toasting.tsx +1 -0
  115. package/src/core/network/types.ts +5 -0
  116. package/src/core/wasm/bridge.ts +1 -0
  117. package/src/plugins/impl/MatrixPlugin.tsx +256 -35
  118. package/src/plugins/impl/__tests__/DataEditorPlugin.test.ts +13 -1
  119. package/src/plugins/impl/__tests__/MatrixPlugin.test.tsx +540 -3
  120. package/src/plugins/impl/data-editor/glide-data-editor.tsx +1 -0
  121. package/src/plugins/impl/data-editor/glide-utils.ts +1 -0
  122. package/src/plugins/impl/data-frames/__tests__/schema.test.ts +9 -1
  123. package/src/plugins/impl/data-frames/utils/__tests__/operators.test.ts +8 -0
  124. package/src/plugins/impl/data-frames/utils/operators.ts +1 -1
  125. package/src/plugins/impl/vega/__tests__/vega.test.ts +22 -4
  126. package/src/plugins/impl/vega/utils.ts +3 -0
  127. package/dist/assets/app-config-button-C2zJXSrK.js +0 -1
  128. package/dist/assets/cell-link-D26WjdN1.js +0 -1
  129. package/dist/assets/config-EoJoeZuK.js +0 -1
  130. package/dist/assets/dependency-graph-panel-CH6kIoat.js +0 -5
  131. package/dist/assets/index-C5WESBbZ.js +0 -38
  132. package/dist/assets/index-WoaGtlKd.css +0 -2
  133. package/dist/assets/panel-accordion-state-0CKLFeAf.js +0 -1
  134. package/dist/assets/spec-NeCHOv2Z.js +0 -1
  135. package/dist/assets/state-Ci7n55A9.js +0 -3
  136. package/dist/assets/useNotebookActions-Bm9VzJ_M.js +0 -12
@@ -9,10 +9,22 @@ describe("DataEditorPlugin", () => {
9
9
  initialValue: { edits: [] },
10
10
  label: null,
11
11
  data: [],
12
- fieldTypes: [["geom", ["geometry", "geometry"]]],
12
+ fieldTypes: [["geom", ["bogus_type", "geometry"]]],
13
13
  editableColumns: "all",
14
14
  });
15
15
 
16
16
  expect(result.fieldTypes).toEqual([["geom", ["unknown", "geometry"]]]);
17
17
  });
18
+
19
+ it("keeps the geometry field type", () => {
20
+ const result = DataEditorPlugin.validator.parse({
21
+ initialValue: { edits: [] },
22
+ label: null,
23
+ data: [],
24
+ fieldTypes: [["geom", ["geometry", "geometry"]]],
25
+ editableColumns: "all",
26
+ });
27
+
28
+ expect(result.fieldTypes).toEqual([["geom", ["geometry", "geometry"]]]);
29
+ });
18
30
  });
@@ -177,7 +177,7 @@ describe("MatrixPlugin", () => {
177
177
 
178
178
  it("validates with zod schema", () => {
179
179
  const plugin = new MatrixPlugin();
180
- const result = plugin.validator.safeParse({
180
+ const payload = {
181
181
  initialValue: [
182
182
  [1, 2],
183
183
  [3, 4],
@@ -198,8 +198,12 @@ describe("MatrixPlugin", () => {
198
198
  [false, false],
199
199
  [false, false],
200
200
  ],
201
- });
202
- expect(result.success).toBe(true);
201
+ };
202
+ // debounce is optional and defaults off
203
+ expect(plugin.validator.parse(payload).debounce).toBe(false);
204
+ expect(plugin.validator.safeParse({ ...payload, step: null }).success).toBe(
205
+ false,
206
+ );
203
207
  });
204
208
 
205
209
  it("displays values in scientific notation", () => {
@@ -297,6 +301,26 @@ describe("MatrixPlugin", () => {
297
301
  ]);
298
302
  });
299
303
 
304
+ it("ArrowUp preserves significant digits in large cell values", () => {
305
+ const plugin = new MatrixPlugin();
306
+ const setValueMock = vi.fn();
307
+ const props = makeProps({
308
+ value: [
309
+ [1_234_567_890_123, 0],
310
+ [0, 0],
311
+ ],
312
+ setValue: setValueMock,
313
+ });
314
+ const { getByTestId } = render(plugin.render(props));
315
+
316
+ fireEvent.keyDown(getByTestId("matrix-cell-0-0"), { key: "ArrowUp" });
317
+
318
+ expect(setValueMock).toHaveBeenCalledWith([
319
+ [1_234_567_890_124, 0],
320
+ [0, 0],
321
+ ]);
322
+ });
323
+
300
324
  it("ArrowDown decrements cell value", () => {
301
325
  const plugin = new MatrixPlugin();
302
326
  const setValueMock = vi.fn();
@@ -317,6 +341,45 @@ describe("MatrixPlugin", () => {
317
341
  ]);
318
342
  });
319
343
 
344
+ it("ArrowRight and ArrowLeft step like ArrowUp and ArrowDown", () => {
345
+ const plugin = new MatrixPlugin();
346
+ const setValueMock = vi.fn();
347
+ const props = makeProps({
348
+ value: [
349
+ [5, 0],
350
+ [0, 0],
351
+ ],
352
+ setValue: setValueMock,
353
+ });
354
+ const { getByTestId } = render(plugin.render(props));
355
+ const cell = getByTestId("matrix-cell-0-0");
356
+
357
+ fireEvent.keyDown(cell, { key: "ArrowRight" });
358
+ expect(setValueMock).toHaveBeenLastCalledWith([
359
+ [6, 0],
360
+ [0, 0],
361
+ ]);
362
+
363
+ // Modifier scaling applies to the horizontal pair too: 5 - 10x step
364
+ fireEvent.keyDown(cell, { key: "ArrowLeft", shiftKey: true });
365
+ expect(setValueMock).toHaveBeenLastCalledWith([
366
+ [-5, 0],
367
+ [0, 0],
368
+ ]);
369
+ });
370
+
371
+ it("clicking a cell focuses it for keyboard stepping", () => {
372
+ const plugin = new MatrixPlugin();
373
+ const props = makeProps();
374
+ const { getByTestId } = render(plugin.render(props));
375
+ const cell = getByTestId("matrix-cell-0-0");
376
+
377
+ fireEvent.pointerDown(cell, { clientX: 100, pointerId: 1 });
378
+ fireEvent.pointerUp(getByTestId("marimo-plugin-matrix"));
379
+
380
+ expect(document.activeElement).toBe(cell);
381
+ });
382
+
320
383
  it("disabled cells ignore pointer and keyboard input", () => {
321
384
  const plugin = new MatrixPlugin();
322
385
  const setValueMock = vi.fn();
@@ -386,6 +449,38 @@ describe("MatrixPlugin", () => {
386
449
  ]);
387
450
  });
388
451
 
452
+ it("with debounce, defers setValue until the drag ends", () => {
453
+ const plugin = new MatrixPlugin();
454
+ const setValueMock = vi.fn();
455
+ const props = makeProps({
456
+ value: [
457
+ [0, 0],
458
+ [0, 0],
459
+ ],
460
+ setValue: setValueMock,
461
+ data: {
462
+ ...makeProps().data,
463
+ debounce: true,
464
+ },
465
+ });
466
+ const { getByTestId } = render(plugin.render(props));
467
+ const cell = getByTestId("matrix-cell-0-0");
468
+ const container = getByTestId("marimo-plugin-matrix");
469
+
470
+ fireEvent.pointerDown(cell, { clientX: 100, pointerId: 1 });
471
+ fireEvent.pointerMove(container, { clientX: 130 });
472
+
473
+ // The dragged value renders locally but is not emitted yet.
474
+ expect(cell.textContent).toBe("3.0");
475
+ expect(setValueMock).not.toHaveBeenCalled();
476
+
477
+ fireEvent.pointerUp(container);
478
+ expect(setValueMock).toHaveBeenCalledExactlyOnceWith([
479
+ [3, 0],
480
+ [0, 0],
481
+ ]);
482
+ });
483
+
389
484
  it("sets aria attributes on cells", () => {
390
485
  const plugin = new MatrixPlugin();
391
486
  const props = makeProps({
@@ -413,3 +508,445 @@ describe("MatrixPlugin", () => {
413
508
  expect(cell.getAttribute("tabindex")).toBe("0");
414
509
  });
415
510
  });
511
+
512
+ describe("MatrixPlugin cell editing", () => {
513
+ it("double-click opens an editor prefilled with the exact value", () => {
514
+ const plugin = new MatrixPlugin();
515
+ const props = makeProps();
516
+ const { getByTestId } = render(plugin.render(props));
517
+
518
+ fireEvent.doubleClick(getByTestId("matrix-cell-0-0"));
519
+
520
+ const input = getByTestId("matrix-input-0-0") as HTMLInputElement;
521
+ expect(input.value).toBe("1");
522
+ });
523
+
524
+ it("commits a typed scientific-notation value on Enter", () => {
525
+ const plugin = new MatrixPlugin();
526
+ const setValueMock = vi.fn();
527
+ const props = makeProps({ setValue: setValueMock });
528
+ const { getByTestId, queryByTestId } = render(plugin.render(props));
529
+
530
+ fireEvent.doubleClick(getByTestId("matrix-cell-0-0"));
531
+ const input = getByTestId("matrix-input-0-0");
532
+ fireEvent.change(input, { target: { value: "2.32e7" } });
533
+ fireEvent.keyDown(input, { key: "Enter" });
534
+
535
+ // Exactly once: the blur fired while refocusing the cell must not
536
+ // commit a second time.
537
+ expect(setValueMock).toHaveBeenCalledExactlyOnceWith([
538
+ [23_200_000, 2],
539
+ [3, 4],
540
+ ]);
541
+ expect(queryByTestId("matrix-input-0-0")).toBeNull();
542
+ });
543
+
544
+ it("does not snap typed values to step", () => {
545
+ const plugin = new MatrixPlugin();
546
+ const setValueMock = vi.fn();
547
+ const props = makeProps({ setValue: setValueMock });
548
+ const { getByTestId } = render(plugin.render(props));
549
+
550
+ fireEvent.doubleClick(getByTestId("matrix-cell-0-0"));
551
+ const input = getByTestId("matrix-input-0-0");
552
+ fireEvent.change(input, { target: { value: "2.5" } });
553
+ fireEvent.keyDown(input, { key: "Enter" });
554
+
555
+ // step is 1, but the typed value is exact
556
+ expect(setValueMock).toHaveBeenCalledWith([
557
+ [2.5, 2],
558
+ [3, 4],
559
+ ]);
560
+ });
561
+
562
+ it("commits on blur", () => {
563
+ const plugin = new MatrixPlugin();
564
+ const setValueMock = vi.fn();
565
+ const props = makeProps({ setValue: setValueMock });
566
+ const { getByTestId, queryByTestId } = render(plugin.render(props));
567
+
568
+ fireEvent.doubleClick(getByTestId("matrix-cell-0-0"));
569
+ const input = getByTestId("matrix-input-0-0");
570
+ fireEvent.change(input, { target: { value: "42" } });
571
+ fireEvent.blur(input);
572
+
573
+ expect(setValueMock).toHaveBeenCalledWith([
574
+ [42, 2],
575
+ [3, 4],
576
+ ]);
577
+ expect(queryByTestId("matrix-input-0-0")).toBeNull();
578
+ });
579
+
580
+ it("Escape cancels without committing", () => {
581
+ const plugin = new MatrixPlugin();
582
+ const setValueMock = vi.fn();
583
+ const props = makeProps({ setValue: setValueMock });
584
+ const { getByTestId, queryByTestId } = render(plugin.render(props));
585
+
586
+ fireEvent.doubleClick(getByTestId("matrix-cell-0-0"));
587
+ const input = getByTestId("matrix-input-0-0");
588
+ fireEvent.change(input, { target: { value: "42" } });
589
+ fireEvent.keyDown(input, { key: "Escape" });
590
+
591
+ expect(setValueMock).not.toHaveBeenCalled();
592
+ expect(queryByTestId("matrix-input-0-0")).toBeNull();
593
+ });
594
+
595
+ it("discards invalid input and closes the editor", () => {
596
+ const plugin = new MatrixPlugin();
597
+ const setValueMock = vi.fn();
598
+ const props = makeProps({ setValue: setValueMock });
599
+ const { getByTestId, queryByTestId } = render(plugin.render(props));
600
+
601
+ fireEvent.doubleClick(getByTestId("matrix-cell-0-0"));
602
+ const input = getByTestId("matrix-input-0-0");
603
+ fireEvent.change(input, { target: { value: "abc" } });
604
+ fireEvent.keyDown(input, { key: "Enter" });
605
+
606
+ expect(setValueMock).not.toHaveBeenCalled();
607
+ expect(queryByTestId("matrix-input-0-0")).toBeNull();
608
+ });
609
+
610
+ it("clamps typed values to bounds", () => {
611
+ const plugin = new MatrixPlugin();
612
+ const setValueMock = vi.fn();
613
+ const props = makeProps({
614
+ setValue: setValueMock,
615
+ data: {
616
+ ...makeProps().data,
617
+ maxValue: [
618
+ [10, 10],
619
+ [10, 10],
620
+ ],
621
+ },
622
+ });
623
+ const { getByTestId } = render(plugin.render(props));
624
+
625
+ fireEvent.doubleClick(getByTestId("matrix-cell-0-0"));
626
+ const input = getByTestId("matrix-input-0-0");
627
+ fireEvent.change(input, { target: { value: "50" } });
628
+ fireEvent.keyDown(input, { key: "Enter" });
629
+
630
+ expect(setValueMock).toHaveBeenCalledWith([
631
+ [10, 2],
632
+ [3, 4],
633
+ ]);
634
+ });
635
+
636
+ it("mirrors typed values in symmetric mode", () => {
637
+ const plugin = new MatrixPlugin();
638
+ const setValueMock = vi.fn();
639
+ const props = makeProps({
640
+ value: [
641
+ [0, 0],
642
+ [0, 0],
643
+ ],
644
+ setValue: setValueMock,
645
+ data: {
646
+ ...makeProps().data,
647
+ symmetric: true,
648
+ },
649
+ });
650
+ const { getByTestId } = render(plugin.render(props));
651
+
652
+ fireEvent.doubleClick(getByTestId("matrix-cell-0-1"));
653
+ const input = getByTestId("matrix-input-0-1");
654
+ fireEvent.change(input, { target: { value: "7" } });
655
+ fireEvent.keyDown(input, { key: "Enter" });
656
+
657
+ expect(setValueMock).toHaveBeenCalledWith([
658
+ [0, 7],
659
+ [7, 0],
660
+ ]);
661
+ });
662
+
663
+ it("keeps the symmetric mirror within the transpose cell's bounds", () => {
664
+ // Asymmetric per-cell bounds on a symmetric matrix: [0][1] allows up to
665
+ // 100, but its transpose [1][0] is capped at 5. Editing [0][1] must not
666
+ // smuggle an out-of-bounds value into the mirror, and the pair must stay
667
+ // equal — so the shared value is clamped to the intersection (max 5).
668
+ const plugin = new MatrixPlugin();
669
+ const setValueMock = vi.fn();
670
+ const props = makeProps({
671
+ value: [
672
+ [0, 0],
673
+ [0, 0],
674
+ ],
675
+ setValue: setValueMock,
676
+ data: {
677
+ ...makeProps().data,
678
+ symmetric: true,
679
+ maxValue: [
680
+ [100, 100],
681
+ [5, 100],
682
+ ],
683
+ },
684
+ });
685
+ const { getByTestId } = render(plugin.render(props));
686
+
687
+ fireEvent.doubleClick(getByTestId("matrix-cell-0-1"));
688
+ const input = getByTestId("matrix-input-0-1");
689
+ fireEvent.change(input, { target: { value: "7" } });
690
+ fireEvent.keyDown(input, { key: "Enter" });
691
+
692
+ expect(setValueMock).toHaveBeenCalledWith([
693
+ [0, 5],
694
+ [5, 0],
695
+ ]);
696
+ });
697
+
698
+ it("emits nothing when clamping leaves the value unchanged", () => {
699
+ // Already at the transpose's cap of 5: dragging or typing past it must
700
+ // not fire setValue with an identical matrix.
701
+ const plugin = new MatrixPlugin();
702
+ const setValueMock = vi.fn();
703
+ const props = makeProps({
704
+ value: [
705
+ [0, 5],
706
+ [5, 0],
707
+ ],
708
+ setValue: setValueMock,
709
+ data: {
710
+ ...makeProps().data,
711
+ symmetric: true,
712
+ maxValue: [
713
+ [100, 100],
714
+ [5, 100],
715
+ ],
716
+ },
717
+ });
718
+ const { getByTestId } = render(plugin.render(props));
719
+ const cell = getByTestId("matrix-cell-0-1");
720
+ const container = getByTestId("marimo-plugin-matrix");
721
+
722
+ fireEvent.pointerDown(cell, { clientX: 100, pointerId: 1 });
723
+ fireEvent.pointerMove(container, { clientX: 130 });
724
+ fireEvent.pointerUp(container);
725
+
726
+ fireEvent.doubleClick(cell);
727
+ const input = getByTestId("matrix-input-0-1");
728
+ fireEvent.change(input, { target: { value: "7" } });
729
+ fireEvent.keyDown(input, { key: "Enter" });
730
+
731
+ expect(setValueMock).not.toHaveBeenCalled();
732
+ });
733
+
734
+ it("Enter opens the editor from the keyboard", () => {
735
+ const plugin = new MatrixPlugin();
736
+ const props = makeProps();
737
+ const { getByTestId } = render(plugin.render(props));
738
+
739
+ fireEvent.keyDown(getByTestId("matrix-cell-0-0"), { key: "Enter" });
740
+
741
+ const input = getByTestId("matrix-input-0-0") as HTMLInputElement;
742
+ expect(input.value).toBe("1");
743
+ });
744
+
745
+ it("typing a digit starts editing seeded with that digit", () => {
746
+ const plugin = new MatrixPlugin();
747
+ const props = makeProps();
748
+ const { getByTestId } = render(plugin.render(props));
749
+
750
+ fireEvent.keyDown(getByTestId("matrix-cell-0-0"), { key: "5" });
751
+
752
+ const input = getByTestId("matrix-input-0-0") as HTMLInputElement;
753
+ expect(input.value).toBe("5");
754
+ });
755
+
756
+ it("does not open an editor on disabled cells", () => {
757
+ const plugin = new MatrixPlugin();
758
+ const props = makeProps({
759
+ data: {
760
+ ...makeProps().data,
761
+ disabled: [
762
+ [true, false],
763
+ [false, false],
764
+ ],
765
+ },
766
+ });
767
+ const { getByTestId, queryByTestId } = render(plugin.render(props));
768
+
769
+ fireEvent.doubleClick(getByTestId("matrix-cell-0-0"));
770
+ expect(queryByTestId("matrix-input-0-0")).toBeNull();
771
+
772
+ fireEvent.keyDown(getByTestId("matrix-cell-0-0"), { key: "Enter" });
773
+ expect(queryByTestId("matrix-input-0-0")).toBeNull();
774
+ });
775
+ });
776
+
777
+ describe("MatrixPlugin modifier scrubbing", () => {
778
+ it("shift+drag scrubs at 10x step", () => {
779
+ const plugin = new MatrixPlugin();
780
+ const setValueMock = vi.fn();
781
+ const props = makeProps({
782
+ value: [
783
+ [0, 0],
784
+ [0, 0],
785
+ ],
786
+ setValue: setValueMock,
787
+ });
788
+ const { getByTestId } = render(plugin.render(props));
789
+ const cell = getByTestId("matrix-cell-0-0");
790
+ const container = getByTestId("marimo-plugin-matrix");
791
+
792
+ fireEvent.pointerDown(cell, { clientX: 100, pointerId: 1, shiftKey: true });
793
+ fireEvent.pointerMove(container, { clientX: 130, shiftKey: true });
794
+ fireEvent.pointerUp(container);
795
+
796
+ expect(setValueMock).toHaveBeenCalledWith([
797
+ [30, 0],
798
+ [0, 0],
799
+ ]);
800
+ });
801
+
802
+ it("alt+drag scrubs at 0.1x step", () => {
803
+ const plugin = new MatrixPlugin();
804
+ const setValueMock = vi.fn();
805
+ const props = makeProps({
806
+ value: [
807
+ [0, 0],
808
+ [0, 0],
809
+ ],
810
+ setValue: setValueMock,
811
+ });
812
+ const { getByTestId } = render(plugin.render(props));
813
+ const cell = getByTestId("matrix-cell-0-0");
814
+ const container = getByTestId("marimo-plugin-matrix");
815
+
816
+ fireEvent.pointerDown(cell, { clientX: 100, pointerId: 1, altKey: true });
817
+ fireEvent.pointerMove(container, { clientX: 130, altKey: true });
818
+ fireEvent.pointerUp(container);
819
+
820
+ expect(setValueMock).toHaveBeenCalledWith([
821
+ [0.3, 0],
822
+ [0, 0],
823
+ ]);
824
+ });
825
+
826
+ it("pressing shift mid-drag rescales future movement without jumping", () => {
827
+ const plugin = new MatrixPlugin();
828
+ const setValueMock = vi.fn();
829
+ const props = makeProps({
830
+ value: [
831
+ [0, 0],
832
+ [0, 0],
833
+ ],
834
+ setValue: setValueMock,
835
+ });
836
+ const { getByTestId } = render(plugin.render(props));
837
+ const cell = getByTestId("matrix-cell-0-0");
838
+ const container = getByTestId("marimo-plugin-matrix");
839
+
840
+ fireEvent.pointerDown(cell, { clientX: 100, pointerId: 1 });
841
+ fireEvent.pointerMove(container, { clientX: 130 }); // +3
842
+ fireEvent.pointerMove(container, { clientX: 130, shiftKey: true }); // rebase
843
+ fireEvent.pointerMove(container, { clientX: 160, shiftKey: true }); // +30
844
+ fireEvent.pointerUp(container);
845
+
846
+ expect(setValueMock).toHaveBeenLastCalledWith([
847
+ [33, 0],
848
+ [0, 0],
849
+ ]);
850
+ });
851
+
852
+ it("shift+ArrowUp increments by 10x step", () => {
853
+ const plugin = new MatrixPlugin();
854
+ const setValueMock = vi.fn();
855
+ const props = makeProps({
856
+ value: [
857
+ [5, 0],
858
+ [0, 0],
859
+ ],
860
+ setValue: setValueMock,
861
+ });
862
+ const { getByTestId } = render(plugin.render(props));
863
+
864
+ fireEvent.keyDown(getByTestId("matrix-cell-0-0"), {
865
+ key: "ArrowUp",
866
+ shiftKey: true,
867
+ });
868
+
869
+ expect(setValueMock).toHaveBeenCalledWith([
870
+ [15, 0],
871
+ [0, 0],
872
+ ]);
873
+ });
874
+
875
+ it("alt+ArrowDown decrements by 0.1x step", () => {
876
+ const plugin = new MatrixPlugin();
877
+ const setValueMock = vi.fn();
878
+ const props = makeProps({
879
+ value: [
880
+ [5, 0],
881
+ [0, 0],
882
+ ],
883
+ setValue: setValueMock,
884
+ });
885
+ const { getByTestId } = render(plugin.render(props));
886
+
887
+ fireEvent.keyDown(getByTestId("matrix-cell-0-0"), {
888
+ key: "ArrowDown",
889
+ altKey: true,
890
+ });
891
+
892
+ expect(setValueMock).toHaveBeenCalledWith([
893
+ [4.9, 0],
894
+ [0, 0],
895
+ ]);
896
+ });
897
+
898
+ it("keeps tiny step values instead of rounding them to zero", () => {
899
+ // 2e-14 sits below the absolute noise threshold for values of ordinary
900
+ // magnitude; the tolerance must scale with the step to preserve it.
901
+ const plugin = new MatrixPlugin();
902
+ const setValueMock = vi.fn();
903
+ const props = makeProps({
904
+ value: [
905
+ [0, 0],
906
+ [0, 0],
907
+ ],
908
+ setValue: setValueMock,
909
+ data: {
910
+ ...makeProps().data,
911
+ step: [
912
+ [2e-14, 1],
913
+ [1, 1],
914
+ ],
915
+ },
916
+ });
917
+ const { getByTestId } = render(plugin.render(props));
918
+
919
+ fireEvent.keyDown(getByTestId("matrix-cell-0-0"), { key: "ArrowUp" });
920
+
921
+ expect(setValueMock).toHaveBeenCalledWith([
922
+ [2e-14, 0],
923
+ [0, 0],
924
+ ]);
925
+ });
926
+
927
+ it("PageUp and PageDown step by 10x", () => {
928
+ const plugin = new MatrixPlugin();
929
+ const setValueMock = vi.fn();
930
+ const props = makeProps({
931
+ value: [
932
+ [5, 0],
933
+ [0, 0],
934
+ ],
935
+ setValue: setValueMock,
936
+ });
937
+ const { getByTestId } = render(plugin.render(props));
938
+ const cell = getByTestId("matrix-cell-0-0");
939
+
940
+ fireEvent.keyDown(cell, { key: "PageUp" });
941
+ expect(setValueMock).toHaveBeenCalledWith([
942
+ [15, 0],
943
+ [0, 0],
944
+ ]);
945
+
946
+ fireEvent.keyDown(cell, { key: "PageDown" });
947
+ expect(setValueMock).toHaveBeenCalledWith([
948
+ [-5, 0],
949
+ [0, 0],
950
+ ]);
951
+ });
952
+ });
@@ -390,6 +390,7 @@ export const GlideDataEditor = <T,>({
390
390
  // TODO: Handle specific types
391
391
  return [column.title, new Date()];
392
392
  case "string":
393
+ case "geometry":
393
394
  case "unknown":
394
395
  return [column.title, ""];
395
396
  default:
@@ -43,6 +43,7 @@ export function getColumnHeaderIcon(fieldType: DataType): GridColumnIcon {
43
43
  return GridColumnIcon.HeaderDate;
44
44
  case "time":
45
45
  return GridColumnIcon.HeaderTime;
46
+ case "geometry":
46
47
  case "unknown":
47
48
  return GridColumnIcon.HeaderString;
48
49
  default:
@@ -18,7 +18,7 @@ describe("columnToFieldTypesSchema", () => {
18
18
 
19
19
  it("normalizes an unrecognized field type without discarding other fields", () => {
20
20
  const result = columnToFieldTypesSchema.parse([
21
- ["geom", ["geometry", "geometry"]],
21
+ ["geom", ["bogus_type", "geometry"]],
22
22
  ["label", ["string", "object"]],
23
23
  ]);
24
24
 
@@ -27,4 +27,12 @@ describe("columnToFieldTypesSchema", () => {
27
27
  ["label", ["string", "object"]],
28
28
  ]);
29
29
  });
30
+
31
+ it("keeps the geometry field type", () => {
32
+ const result = columnToFieldTypesSchema.parse([
33
+ ["geom", ["geometry", "geometry"]],
34
+ ]);
35
+
36
+ expect(result).toEqual([["geom", ["geometry", "geometry"]]]);
37
+ });
30
38
  });
@@ -60,6 +60,10 @@ describe("getOperatorForDtype", () => {
60
60
  it("should return empty array for unknown dtype", () => {
61
61
  expect(getOperatorForDtype("unknown")).toEqual([]);
62
62
  });
63
+
64
+ it("should return no operators for geometry", () => {
65
+ expect(getOperatorForDtype("geometry")).toEqual([]);
66
+ });
63
67
  });
64
68
 
65
69
  describe("getSchemaForOperator", () => {
@@ -87,6 +91,10 @@ describe("getSchemaForOperator", () => {
87
91
  expect(getSchemaForOperator("unknown", "==")).toEqual([]);
88
92
  expect(getSchemaForOperator("int", "unknown")).toEqual([]);
89
93
  });
94
+
95
+ it("should return no operand schema for geometry", () => {
96
+ expect(getSchemaForOperator("geometry", "==")).toEqual([]);
97
+ });
90
98
  });
91
99
 
92
100
  describe("isConditionValueValid", () => {
@@ -76,7 +76,7 @@ export const ALL_OPERATORS = {
76
76
 
77
77
  export type OperatorType = keyof typeof ALL_OPERATORS;
78
78
 
79
- function numpyTypeToDataType(type: string): DataType {
79
+ function numpyTypeToDataType(type: string): Exclude<DataType, "geometry"> {
80
80
  if (!type) {
81
81
  return "unknown";
82
82
  }