@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,13 +9,18 @@ import type {
9
9
  } from "@/core/kernel/messages";
10
10
  import { renderColumnInfo, renderTableInfo } from "../renderers";
11
11
 
12
- const geometryColumn = {
12
+ const bogusColumn = {
13
13
  name: "geom",
14
- type: "geometry" as DataType,
14
+ type: "bogus_type" as DataType,
15
15
  external_type: "geometry",
16
16
  sample_values: [],
17
17
  } as DataTableColumn;
18
18
 
19
+ const geometryColumn = {
20
+ ...bogusColumn,
21
+ type: "geometry" as DataType,
22
+ } as DataTableColumn;
23
+
19
24
  describe("SQL renderers", () => {
20
25
  it("renders table information with an unrecognized column type", () => {
21
26
  const table: DataTable = {
@@ -26,7 +31,7 @@ describe("SQL renderers", () => {
26
31
  num_rows: null,
27
32
  num_columns: 1,
28
33
  variable_name: null,
29
- columns: [geometryColumn],
34
+ columns: [bogusColumn],
30
35
  primary_keys: [],
31
36
  indexes: [],
32
37
  };
@@ -37,8 +42,14 @@ describe("SQL renderers", () => {
37
42
  });
38
43
 
39
44
  it("renders column information with an unrecognized column type", () => {
40
- const result = render(renderColumnInfo(geometryColumn));
45
+ const result = render(renderColumnInfo(bogusColumn));
41
46
 
42
47
  expect(result.container.querySelector("svg")).not.toBeNull();
43
48
  });
49
+
50
+ it("renders geometry column information with the map pin icon", () => {
51
+ const result = render(renderColumnInfo(geometryColumn));
52
+
53
+ expect(result.container.querySelector(".lucide-map-pin")).not.toBeNull();
54
+ });
44
55
  });
@@ -41,6 +41,7 @@ const DATA_TYPE_COLORS: Record<DataType, string> = {
41
41
  number: "bg-(--purple-4) text-(--purple-11)",
42
42
  integer: "bg-(--purple-4) text-(--purple-11)",
43
43
  string: "bg-(--blue-4) text-(--blue-11)",
44
+ geometry: "bg-(--cyan-4) text-(--cyan-11)",
44
45
  unknown: "bg-(--slate-4) text-(--slate-11)",
45
46
  };
46
47
 
@@ -356,6 +356,7 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
356
356
  sendFileDetails = throwNotImplemented;
357
357
  openTutorial = throwNotImplemented;
358
358
  getExportAvailability = throwNotImplemented;
359
+ installExportRequirements = throwNotImplemented;
359
360
  exportAsHTML = throwNotImplemented;
360
361
  exportAsIPYNB = throwNotImplemented;
361
362
  exportAsMarkdown = throwNotImplemented;
@@ -11,6 +11,7 @@ export const DATA_TYPES = [
11
11
  "date",
12
12
  "datetime",
13
13
  "time",
14
+ "geometry",
14
15
  "unknown",
15
16
  ] as const;
16
17
  export type ModelLifecycle = NotificationMessageData<"model-lifecycle">;
package/src/core/mode.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import { atom, useAtomValue } from "jotai";
4
+ import { KnownQueryParams } from "@/core/constants";
4
5
  import { isIslands } from "@/core/islands/utils";
5
6
  import { assertExists } from "@/utils/assertExists";
6
7
  import { invariant } from "@/utils/invariant";
@@ -98,5 +99,9 @@ export const kioskModeAtom = atom<boolean>(false);
98
99
  */
99
100
  export function useInstallAllowed(): boolean {
100
101
  const { mode } = useAtomValue(viewStateAtom);
101
- return mode !== "read";
102
+ const kioskMode = useAtomValue(kioskModeAtom);
103
+ const kioskRequested =
104
+ new URLSearchParams(window.location.search).get(KnownQueryParams.kiosk) ===
105
+ "true";
106
+ return mode !== "read" && !kioskMode && !kioskRequested;
102
107
  }
@@ -2,7 +2,9 @@
2
2
 
3
3
  import { beforeEach, describe, expect, it, vi } from "vitest";
4
4
  import { cellId, requestId, uiElementId } from "@/__tests__/branded";
5
+ import { waitForKernelToBeInstantiated } from "../../kernel/state";
5
6
  import type { RuntimeManager } from "../../runtime/runtime";
7
+ import { waitForConnectionOpen } from "../connection";
6
8
  import { createLazyRequests } from "../requests-lazy";
7
9
  import type {
8
10
  EditRequests,
@@ -55,6 +57,10 @@ describe("createLazyRequests", () => {
55
57
  sendInterrupt: vi.fn().mockResolvedValue({ interrupted: true }),
56
58
  sendPdb: vi.fn().mockResolvedValue({ pdb: true }),
57
59
  sendRunScratchpad: vi.fn().mockResolvedValue({ run: true }),
60
+ installExportRequirements: vi.fn().mockResolvedValue({
61
+ source: "server",
62
+ formats: [],
63
+ }),
58
64
  } as unknown as EditRequests & RunRequests;
59
65
  });
60
66
 
@@ -104,6 +110,40 @@ describe("createLazyRequests", () => {
104
110
  expect(mockDelegate.getExportAvailability).toHaveBeenCalledOnce();
105
111
  });
106
112
 
113
+ it("installs export requirements without waiting for the kernel", async () => {
114
+ const lazyRequests = createLazyRequests(
115
+ mockDelegate,
116
+ mockGetRuntimeManager,
117
+ );
118
+
119
+ await lazyRequests.installExportRequirements({ format: "pdf" });
120
+
121
+ expect(mockInit).toHaveBeenCalledOnce();
122
+ expect(waitForConnectionOpen).toHaveBeenCalledOnce();
123
+ expect(waitForKernelToBeInstantiated).not.toHaveBeenCalled();
124
+ expect(mockDelegate.installExportRequirements).toHaveBeenCalledWith({
125
+ format: "pdf",
126
+ });
127
+ });
128
+
129
+ it("sends instantiate without waiting for the kernel", async () => {
130
+ const lazyRequests = createLazyRequests(
131
+ mockDelegate,
132
+ mockGetRuntimeManager,
133
+ );
134
+ const request = {
135
+ objectIds: [uiElementId("obj1")],
136
+ values: [],
137
+ };
138
+
139
+ await lazyRequests.sendInstantiate(request);
140
+
141
+ expect(mockInit).toHaveBeenCalledOnce();
142
+ expect(waitForConnectionOpen).toHaveBeenCalledOnce();
143
+ expect(waitForKernelToBeInstantiated).not.toHaveBeenCalled();
144
+ expect(mockDelegate.sendInstantiate).toHaveBeenCalledWith(request);
145
+ });
146
+
107
147
  it("should call init once before first request", async () => {
108
148
  const lazyRequests = createLazyRequests(
109
149
  mockDelegate,
@@ -143,6 +143,19 @@ describe("createNetworkRequests", () => {
143
143
  expect(mockClient.GET).toHaveBeenCalledWith("/api/export/availability");
144
144
  });
145
145
 
146
+ it("installExportRequirements should POST the export format", async () => {
147
+ const requests = createNetworkRequests();
148
+ await requests.installExportRequirements({ format: "pdf" });
149
+
150
+ expect(mockClient.POST).toHaveBeenCalledWith(
151
+ "/api/export/requirements/install",
152
+ expect.objectContaining({
153
+ body: { format: "pdf" },
154
+ params: expect.anything(),
155
+ }),
156
+ );
157
+ });
158
+
146
159
  it("discoverDataSources should POST to the discovery endpoint", async () => {
147
160
  const requests = createNetworkRequests();
148
161
  const request = { requestId: "discovery-request" } as any;
@@ -26,6 +26,10 @@ type AllRequests = EditRequests & RunRequests;
26
26
  // executing. Use for user-initiated actions that should "just work" and
27
27
  // kick off the kernel if needed (e.g., clicking Run).
28
28
  //
29
+ // - startConnectionWithoutKernel: Initializes the runtime and waits for an open
30
+ // connection. Use for session-scoped server requests that do not need an
31
+ // instantiated kernel.
32
+ //
29
33
  // - waitForConnectionOpen: Waits for an existing connection but won't start one.
30
34
  // Use for operations that depend on a running kernel but shouldn't be the
31
35
  // trigger to start it (e.g., saving, interrupting).
@@ -38,6 +42,7 @@ type Action =
38
42
  | "throwError"
39
43
  | "dropRequest"
40
44
  | "startConnection"
45
+ | "startConnectionWithoutKernel"
41
46
  | "waitForConnectionOpen"
42
47
  | "serverOnly";
43
48
 
@@ -45,7 +50,7 @@ const ACTIONS: Record<keyof AllRequests, Action> = {
45
50
  // These will start a connection if not already connected and then wait until the connection is open
46
51
  sendComponentValues: "startConnection",
47
52
  sendModelValue: "startConnection",
48
- sendInstantiate: "startConnection",
53
+ sendInstantiate: "startConnectionWithoutKernel",
49
54
  sendRun: "startConnection",
50
55
  sendDeleteCell: "startConnection",
51
56
  sendRunScratchpad: "startConnection",
@@ -107,6 +112,9 @@ const ACTIONS: Record<keyof AllRequests, Action> = {
107
112
  getEnvironmentInfo: "serverOnly",
108
113
  getExportAvailability: "serverOnly",
109
114
 
115
+ // Session-scoped server operations
116
+ installExportRequirements: "startConnectionWithoutKernel",
117
+
110
118
  // Home operations throw errors
111
119
  getRecentFiles: "startConnection",
112
120
  getWorkspaceFiles: "startConnection",
@@ -186,13 +194,12 @@ export function createLazyRequests(
186
194
  await waitForKernelToBeInstantiated();
187
195
  return request(...args);
188
196
 
197
+ case "startConnectionWithoutKernel":
189
198
  case "startConnection":
190
199
  // Start connection and wait for it to be open
191
200
  await initOnce(runtimeManager);
192
201
  await waitForConnectionOpen();
193
- if (key !== "sendInstantiate") {
194
- // We don't need to wait for kernel to be instantiated if we are sending an instantiate request
195
- // otherwise we will wait forever
202
+ if (action === "startConnection") {
196
203
  await waitForKernelToBeInstantiated();
197
204
  }
198
205
  return request(...args);
@@ -417,6 +417,14 @@ export function createNetworkRequests(): EditRequests & RunRequests {
417
417
  getExportAvailability: () => {
418
418
  return getClient().GET("/api/export/availability").then(handleResponse);
419
419
  },
420
+ installExportRequirements: (request) => {
421
+ return getClient()
422
+ .POST("/api/export/requirements/install", {
423
+ body: request,
424
+ params: getParams(),
425
+ })
426
+ .then(handleResponse);
427
+ },
420
428
  exportAsHTML: async (request) => {
421
429
  if (
422
430
  process.env.NODE_ENV === "development" ||
@@ -84,6 +84,7 @@ export function createStaticRequests(): EditRequests & RunRequests {
84
84
  getRunningNotebooks: throwNotInEditMode,
85
85
  shutdownSession: throwNotInEditMode,
86
86
  getExportAvailability: throwNotInEditMode,
87
+ installExportRequirements: throwNotInEditMode,
87
88
  exportAsHTML: throwNotInEditMode,
88
89
  exportAsIPYNB: throwNotInEditMode,
89
90
  exportAsMarkdown: throwNotInEditMode,
@@ -65,6 +65,7 @@ export function createErrorToastingRequests(
65
65
  getRunningNotebooks: "Failed to get running notebooks",
66
66
  shutdownSession: "Failed to shutdown session",
67
67
  getExportAvailability: "", // No toast
68
+ installExportRequirements: "Failed to install export requirements",
68
69
  exportAsHTML: "Failed to export HTML",
69
70
  exportAsIPYNB: "Failed to export ipynb",
70
71
  exportAsMarkdown: "Failed to export Markdown",
@@ -29,6 +29,8 @@ export type ExportAsIPYNBRequest = schemas["ExportAsIPYNBRequest"];
29
29
  export type ExportAsScriptRequest = schemas["ExportAsScriptRequest"];
30
30
  export type ExportAsPDFRequest = schemas["ExportAsPDFRequest"];
31
31
  export type ExportAvailabilityResponse = schemas["ExportAvailabilityResponse"];
32
+ export type InstallExportRequirementsRequest =
33
+ schemas["InstallExportRequirementsRequest"];
32
34
  export type UpdateCellOutputsRequest = schemas["UpdateCellOutputsRequest"];
33
35
 
34
36
  export interface ExportedFile<T extends BlobPart = BlobPart> {
@@ -223,6 +225,9 @@ export interface EditRequests {
223
225
  ) => Promise<RunningNotebooksResponse>;
224
226
  // Export requests
225
227
  getExportAvailability: () => Promise<ExportAvailabilityResponse>;
228
+ installExportRequirements: (
229
+ request: InstallExportRequirementsRequest,
230
+ ) => Promise<ExportAvailabilityResponse>;
226
231
  exportAsHTML: (request: ExportAsHTMLRequest) => Promise<ExportedFile<string>>;
227
232
  exportAsIPYNB: (
228
233
  request: ExportAsIPYNBRequest,
@@ -675,6 +675,7 @@ export class PyodideBridge implements RunRequests, EditRequests {
675
675
  getRunningNotebooks = throwNotImplemented;
676
676
  shutdownSession = throwNotImplemented;
677
677
  getExportAvailability = throwNotImplemented;
678
+ installExportRequirements = throwNotImplemented;
678
679
  exportAsIPYNB = throwNotImplemented;
679
680
  exportAsPDF = throwNotImplemented;
680
681
  autoExportAsHTML = throwNotImplemented;
@@ -54,6 +54,40 @@ export class MatrixPlugin implements IPlugin<T, Data> {
54
54
  }
55
55
 
56
56
  const PIXELS_PER_STEP = 10;
57
+ const COARSE_MULTIPLIER = 10;
58
+ const FINE_MULTIPLIER = 0.1;
59
+
60
+ /** Step multiplier from modifier keys: shift = coarse, alt = fine. */
61
+ const stepMultiplier = (e: { shiftKey: boolean; altKey: boolean }): number => {
62
+ if (e.shiftKey) {
63
+ return COARSE_MULTIPLIER;
64
+ }
65
+ if (e.altKey) {
66
+ return FINE_MULTIPLIER;
67
+ }
68
+ return 1;
69
+ };
70
+
71
+ /**
72
+ * Strip floating-point noise from step arithmetic (e.g. 3 * 0.1).
73
+ * The noise tolerance scales with the step so that legitimately tiny
74
+ * results (e.g. from a 2e-14 step) are preserved, while noise left over
75
+ * from arithmetic on ordinary magnitudes (e.g. the 5.55e-17 residue of
76
+ * 0.3 - 3 * 0.1) still snaps away.
77
+ */
78
+ const cleanFloat = (x: number, step: number): number => {
79
+ const rounded = Number(x.toFixed(12));
80
+ const tolerance =
81
+ Number.EPSILON * Math.max(Math.abs(x), Math.abs(step)) * 100;
82
+ return Math.abs(x - rounded) <= tolerance ? rounded : x;
83
+ };
84
+
85
+ interface EditState {
86
+ row: number;
87
+ col: number;
88
+ text: string;
89
+ selectAll: boolean;
90
+ }
57
91
 
58
92
  interface MatrixComponentProps extends Data {
59
93
  value: T;
@@ -80,6 +114,7 @@ const MatrixComponent = ({
80
114
  col: number;
81
115
  startX: number;
82
116
  startValue: number;
117
+ multiplier: number;
83
118
  } | null>(null);
84
119
  const [activeCell, setActiveCell] = useState<{
85
120
  row: number;
@@ -95,6 +130,16 @@ const MatrixComponent = ({
95
130
  }, [value]);
96
131
  const displayValue = activeCell == null ? value : draft;
97
132
 
133
+ // Editing state is mirrored in a ref so that the blur fired while
134
+ // committing (refocusing the cell unmounts the input) can't commit twice.
135
+ const [editing, setEditingState] = useState<EditState | null>(null);
136
+ const editingRef = useRef<EditState | null>(null);
137
+ const inputRef = useRef<HTMLInputElement | null>(null);
138
+ const setEditing = useCallback((edit: EditState | null) => {
139
+ editingRef.current = edit;
140
+ setEditingState(edit);
141
+ }, []);
142
+
98
143
  const formatValue = (val: number) =>
99
144
  scientific ? val.toExponential(precision) : val.toFixed(precision);
100
145
 
@@ -112,22 +157,131 @@ const MatrixComponent = ({
112
157
  [minValue, maxValue],
113
158
  );
114
159
 
160
+ // The single mutation chokepoint for every edit path (drag, arrow, type).
161
+ // Bounds are enforced here so no path can escape them. In symmetric mode
162
+ // the pair must stay equal *and* respect both cells' bounds, so the shared
163
+ // value is clamped to the intersection of the two ranges. Returns null
164
+ // when the clamped value is already in place, so callers can skip
165
+ // redundant updates.
166
+ const withCellValue = useCallback(
167
+ (base: T, row: number, col: number, newValue: number): T | null => {
168
+ const mirrored = symmetric && row !== col;
169
+ let clamped = clampValue(newValue, row, col);
170
+ if (mirrored) {
171
+ clamped = clampValue(clamped, col, row);
172
+ }
173
+ if (clamped === base[row][col]) {
174
+ return null;
175
+ }
176
+ const copy = base.map((r) => [...r]);
177
+ copy[row][col] = clamped;
178
+ if (mirrored) {
179
+ copy[col][row] = clamped;
180
+ }
181
+ return copy;
182
+ },
183
+ [symmetric, clampValue],
184
+ );
185
+
186
+ const startEditing = useCallback(
187
+ (row: number, col: number, seed?: string) => {
188
+ if (disabled[row][col]) {
189
+ return;
190
+ }
191
+ dragState.current = null;
192
+ setActiveCell(null);
193
+ setEditing({
194
+ row,
195
+ col,
196
+ text: seed ?? String(value[row][col]),
197
+ selectAll: seed == null,
198
+ });
199
+ },
200
+ [disabled, value, setEditing],
201
+ );
202
+
203
+ // Callback ref: focus the input when it mounts. When editing starts from
204
+ // a typed character, place the caret after the seed instead of selecting.
205
+ const focusInput = useCallback((el: HTMLInputElement | null) => {
206
+ inputRef.current = el;
207
+ if (el) {
208
+ el.focus();
209
+ if (editingRef.current?.selectAll) {
210
+ el.select();
211
+ } else {
212
+ el.setSelectionRange(el.value.length, el.value.length);
213
+ }
214
+ }
215
+ }, []);
216
+
217
+ const commitEdit = useCallback(
218
+ (refocusCell: boolean) => {
219
+ const edit = editingRef.current;
220
+ if (!edit) {
221
+ return;
222
+ }
223
+ const cell = refocusCell ? inputRef.current?.closest("td") : null;
224
+ setEditing(null);
225
+ const text = edit.text.trim();
226
+ const parsed = Number(text);
227
+ // Typed values are clamped to bounds but not snapped to `step`,
228
+ // so exact values like 2.32e7 survive.
229
+ if (text !== "" && Number.isFinite(parsed)) {
230
+ const copy = withCellValue(value, edit.row, edit.col, parsed);
231
+ if (copy) {
232
+ setDraft(copy);
233
+ setValue(copy);
234
+ }
235
+ }
236
+ cell?.focus();
237
+ },
238
+ [value, withCellValue, setValue, setEditing],
239
+ );
240
+
241
+ const cancelEdit = useCallback(() => {
242
+ const cell = inputRef.current?.closest("td");
243
+ setEditing(null);
244
+ cell?.focus();
245
+ }, [setEditing]);
246
+
247
+ const handleInputKeyDown = useCallback(
248
+ (e: React.KeyboardEvent) => {
249
+ e.stopPropagation();
250
+ if (e.key === "Enter") {
251
+ e.preventDefault();
252
+ commitEdit(true);
253
+ } else if (e.key === "Escape") {
254
+ e.preventDefault();
255
+ cancelEdit();
256
+ }
257
+ },
258
+ [commitEdit, cancelEdit],
259
+ );
260
+
115
261
  const handlePointerDown = useCallback(
116
- (e: React.PointerEvent, row: number, col: number) => {
117
- if (disabled[row][col] || !(e.target instanceof Element)) {
262
+ (e: React.PointerEvent<HTMLTableCellElement>, row: number, col: number) => {
263
+ if (
264
+ disabled[row][col] ||
265
+ editing != null ||
266
+ !(e.target instanceof Element)
267
+ ) {
118
268
  return;
119
269
  }
120
270
  e.preventDefault();
271
+ // preventDefault also suppresses the browser's click-to-focus, so
272
+ // focus the cell explicitly to keep keyboard stepping reachable.
273
+ e.currentTarget.focus();
121
274
  e.target.setPointerCapture(e.pointerId);
122
275
  dragState.current = {
123
276
  row,
124
277
  col,
125
278
  startX: e.clientX,
126
279
  startValue: displayValue[row][col],
280
+ multiplier: stepMultiplier(e),
127
281
  };
128
282
  setActiveCell({ row, col });
129
283
  },
130
- [disabled, displayValue],
284
+ [disabled, editing, displayValue],
131
285
  );
132
286
 
133
287
  const handlePointerMove = useCallback(
@@ -136,26 +290,32 @@ const MatrixComponent = ({
136
290
  if (!state) {
137
291
  return;
138
292
  }
293
+ const multiplier = stepMultiplier(e);
294
+ if (multiplier !== state.multiplier) {
295
+ // Rebase so toggling a modifier mid-drag rescales future movement
296
+ // instead of jumping the value.
297
+ state.startX = e.clientX;
298
+ state.startValue = displayValue[state.row][state.col];
299
+ state.multiplier = multiplier;
300
+ }
139
301
  const { row, col, startX, startValue } = state;
140
302
  const dx = e.clientX - startX;
141
- const cellStep = step[row][col];
303
+ const cellStep = step[row][col] * multiplier;
142
304
  const steps = Math.round(dx / PIXELS_PER_STEP);
143
- const rawValue = startValue + steps * cellStep;
144
- const newValue = clampValue(rawValue, row, col);
145
-
146
- if (newValue !== displayValue[row][col]) {
147
- const copy = displayValue.map((r) => [...r]);
148
- copy[row][col] = newValue;
149
- if (symmetric && row !== col) {
150
- copy[col][row] = newValue;
151
- }
305
+ const copy = withCellValue(
306
+ displayValue,
307
+ row,
308
+ col,
309
+ cleanFloat(startValue + steps * cellStep, cellStep),
310
+ );
311
+ if (copy) {
152
312
  setDraft(copy);
153
313
  if (!debounce) {
154
314
  setValue(copy);
155
315
  }
156
316
  }
157
317
  },
158
- [step, clampValue, displayValue, symmetric, debounce, setValue],
318
+ [step, displayValue, withCellValue, debounce, setValue],
159
319
  );
160
320
 
161
321
  const handlePointerUp = useCallback(() => {
@@ -168,27 +328,61 @@ const MatrixComponent = ({
168
328
 
169
329
  const handleKeyDown = useCallback(
170
330
  (e: React.KeyboardEvent, row: number, col: number) => {
171
- if (e.key === "ArrowUp" || e.key === "ArrowDown") {
172
- if (disabled[row][col]) {
173
- return;
174
- }
331
+ if (disabled[row][col]) {
332
+ return;
333
+ }
334
+ if (e.key === "Enter" || e.key === "F2") {
175
335
  e.preventDefault();
176
- const cellStep = step[row][col];
177
- const delta = e.key === "ArrowUp" ? cellStep : -cellStep;
178
- const newValue = clampValue(displayValue[row][col] + delta, row, col);
179
-
180
- if (newValue !== displayValue[row][col]) {
181
- const copy = displayValue.map((r) => [...r]);
182
- copy[row][col] = newValue;
183
- if (symmetric && row !== col) {
184
- copy[col][row] = newValue;
185
- }
186
- setDraft(copy);
187
- setValue(copy);
188
- }
336
+ startEditing(row, col);
337
+ return;
338
+ }
339
+ // Typing a number starts editing, seeded with the typed character.
340
+ if (
341
+ e.key.length === 1 &&
342
+ /[\d.+-]/.test(e.key) &&
343
+ !e.ctrlKey &&
344
+ !e.metaKey &&
345
+ !e.altKey
346
+ ) {
347
+ e.preventDefault();
348
+ startEditing(row, col, e.key);
349
+ return;
350
+ }
351
+ const cellStep = step[row][col];
352
+ let delta: number;
353
+ switch (e.key) {
354
+ // Right/left mirror up/down, matching the horizontal drag direction
355
+ // (and the native slider convention).
356
+ case "ArrowUp":
357
+ case "ArrowRight":
358
+ delta = cellStep * stepMultiplier(e);
359
+ break;
360
+ case "ArrowDown":
361
+ case "ArrowLeft":
362
+ delta = -cellStep * stepMultiplier(e);
363
+ break;
364
+ case "PageUp":
365
+ delta = cellStep * COARSE_MULTIPLIER;
366
+ break;
367
+ case "PageDown":
368
+ delta = -cellStep * COARSE_MULTIPLIER;
369
+ break;
370
+ default:
371
+ return;
372
+ }
373
+ e.preventDefault();
374
+ const copy = withCellValue(
375
+ displayValue,
376
+ row,
377
+ col,
378
+ cleanFloat(displayValue[row][col] + delta, delta),
379
+ );
380
+ if (copy) {
381
+ setDraft(copy);
382
+ setValue(copy);
189
383
  }
190
384
  },
191
- [disabled, step, displayValue, clampValue, symmetric, setValue],
385
+ [disabled, startEditing, step, displayValue, withCellValue, setValue],
192
386
  );
193
387
 
194
388
  const hasRowLabels = rowLabels != null && rowLabels.length > 0;
@@ -238,6 +432,10 @@ const MatrixComponent = ({
238
432
  const isDisabled = disabled[i][j];
239
433
  const isActive =
240
434
  activeCell?.row === i && activeCell?.col === j;
435
+ const cellEdit =
436
+ editing != null && editing.row === i && editing.col === j
437
+ ? editing
438
+ : null;
241
439
  const rowLabel = rowLabels?.[i] ?? `Row ${i + 1}`;
242
440
  const colLabel = columnLabels?.[j] ?? `Column ${j + 1}`;
243
441
  return (
@@ -247,24 +445,47 @@ const MatrixComponent = ({
247
445
  "relative text-center min-w-14 h-8 px-2 transition-colors touch-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-hidden",
248
446
  isDisabled
249
447
  ? "cursor-default text-muted-foreground"
250
- : "cursor-ew-resize text-link hover:bg-accent",
251
- isActive && "bg-accent",
448
+ : cellEdit
449
+ ? "cursor-text"
450
+ : "cursor-ew-resize text-link hover:bg-accent",
451
+ (isActive || cellEdit != null) && "bg-accent",
252
452
  j === 0 && "bracket-l",
253
453
  j === numCols - 1 && "bracket-r",
254
454
  i === 0 && "bracket-t",
255
455
  i === numRows - 1 && "bracket-b",
256
456
  )}
257
457
  tabIndex={isDisabled ? -1 : 0}
458
+ title={String(cellValue)}
258
459
  aria-label={`${rowLabel}, ${colLabel}`}
259
460
  aria-valuenow={cellValue}
260
461
  aria-valuemin={minValue?.[i]?.[j]}
261
462
  aria-valuemax={maxValue?.[i]?.[j]}
262
463
  aria-disabled={isDisabled || undefined}
263
464
  onPointerDown={(e) => handlePointerDown(e, i, j)}
465
+ onDoubleClick={() => startEditing(i, j)}
264
466
  onKeyDown={(e) => handleKeyDown(e, i, j)}
265
467
  data-testid={`matrix-cell-${i}-${j}`}
266
468
  >
267
- {formatValue(cellValue)}
469
+ {cellEdit ? (
470
+ <input
471
+ ref={focusInput}
472
+ className="bg-transparent text-center font-mono text-sm text-foreground outline-none select-text"
473
+ style={{
474
+ width: `${Math.max(cellEdit.text.length + 2, 6)}ch`,
475
+ }}
476
+ value={cellEdit.text}
477
+ onChange={(e) =>
478
+ setEditing({ ...cellEdit, text: e.target.value })
479
+ }
480
+ onKeyDown={handleInputKeyDown}
481
+ onBlur={() => commitEdit(false)}
482
+ onPointerDown={(e) => e.stopPropagation()}
483
+ aria-label={`Edit ${rowLabel}, ${colLabel}`}
484
+ data-testid={`matrix-input-${i}-${j}`}
485
+ />
486
+ ) : (
487
+ formatValue(cellValue)
488
+ )}
268
489
  </td>
269
490
  );
270
491
  })}