@marimo-team/islands 0.21.2-dev6 → 0.21.2-dev61

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 (152) hide show
  1. package/dist/{ConnectedDataExplorerComponent-D0GoOd_c.js → ConnectedDataExplorerComponent-DrWDbHRV.js} +1 -1
  2. package/dist/{any-language-editor-DlsjUw_l.js → any-language-editor-BRpxklRq.js} +1 -1
  3. package/dist/{copy-DIK6DiIA.js → copy-BjkXCUxP.js} +12 -2
  4. package/dist/{esm-BLobyqMs.js → esm-No_6eSQS.js} +1 -1
  5. package/dist/{glide-data-editor-pZyd9UJ_.js → glide-data-editor-858wsVkd.js} +1 -1
  6. package/dist/main.js +821 -417
  7. package/dist/{spec-Bfvf9Hre.js → spec-oVDndBz4.js} +25 -16
  8. package/dist/style.css +1 -1
  9. package/package.json +1 -1
  10. package/src/__mocks__/notebook.ts +9 -9
  11. package/src/__mocks__/requests.ts +1 -0
  12. package/src/__tests__/branded.ts +20 -0
  13. package/src/components/app-config/user-config-form.tsx +5 -4
  14. package/src/components/data-table/__tests__/utils.test.ts +138 -1
  15. package/src/components/data-table/charts/__tests__/storage.test.ts +7 -7
  16. package/src/components/data-table/context-menu.tsx +9 -5
  17. package/src/components/data-table/data-table.tsx +3 -0
  18. package/src/components/data-table/range-focus/__tests__/atoms.test.ts +8 -2
  19. package/src/components/data-table/range-focus/__tests__/test-utils.ts +2 -0
  20. package/src/components/data-table/range-focus/__tests__/utils.test.ts +82 -8
  21. package/src/components/data-table/range-focus/atoms.ts +2 -2
  22. package/src/components/data-table/range-focus/utils.ts +50 -12
  23. package/src/components/data-table/types.ts +7 -0
  24. package/src/components/data-table/utils.ts +87 -0
  25. package/src/components/editor/__tests__/data-attributes.test.tsx +8 -8
  26. package/src/components/editor/ai/__tests__/completion-utils.test.ts +15 -15
  27. package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +2 -2
  28. package/src/components/editor/connections/storage/as-code.ts +2 -2
  29. package/src/components/editor/file-tree/file-explorer.tsx +16 -2
  30. package/src/components/editor/file-tree/file-viewer.tsx +17 -3
  31. package/src/components/editor/navigation/__tests__/clipboard.test.ts +2 -2
  32. package/src/components/editor/navigation/__tests__/selection.test.ts +7 -6
  33. package/src/components/editor/navigation/__tests__/state.test.ts +8 -7
  34. package/src/components/editor/output/MarimoErrorOutput.tsx +7 -7
  35. package/src/components/editor/output/__tests__/traceback.test.tsx +4 -4
  36. package/src/components/editor/output/console/__tests__/ConsoleOutput.test.tsx +4 -4
  37. package/src/components/editor/renderers/vertical-layout/useFocusFirstEditor.ts +8 -1
  38. package/src/components/storage/storage-file-viewer.tsx +35 -1
  39. package/src/components/storage/storage-inspector.tsx +9 -4
  40. package/src/components/storage/storage-snippets.ts +3 -3
  41. package/src/components/tracing/tracing.tsx +3 -1
  42. package/src/components/ui/range-slider.tsx +108 -1
  43. package/src/core/ai/__tests__/staged-cells.test.ts +9 -8
  44. package/src/core/ai/context/providers/__tests__/cell-output.test.ts +31 -31
  45. package/src/core/ai/context/providers/__tests__/datasource.test.ts +3 -3
  46. package/src/core/ai/context/providers/__tests__/tables.test.ts +3 -2
  47. package/src/core/ai/context/providers/__tests__/variable.test.ts +84 -63
  48. package/src/core/ai/tools/__tests__/edit-notebook-tool.test.ts +10 -9
  49. package/src/core/ai/tools/__tests__/run-cells-tool.test.ts +6 -6
  50. package/src/core/ai/tools/edit-notebook-tool.ts +3 -3
  51. package/src/core/cells/__tests__/add-missing-import.test.ts +3 -3
  52. package/src/core/cells/__tests__/apply-transaction.test.ts +279 -0
  53. package/src/core/cells/__tests__/cells.test.ts +198 -135
  54. package/src/core/cells/__tests__/document-changes.test.ts +575 -0
  55. package/src/core/cells/__tests__/document-roundtrip.test.ts +376 -0
  56. package/src/core/cells/__tests__/focus.test.ts +5 -4
  57. package/src/core/cells/__tests__/logs.test.ts +13 -12
  58. package/src/core/cells/__tests__/pending-delete-service.test.tsx +3 -3
  59. package/src/core/cells/__tests__/runs.test.ts +22 -21
  60. package/src/core/cells/__tests__/scrollCellIntoView.test.ts +8 -7
  61. package/src/core/cells/__tests__/session.test.ts +23 -22
  62. package/src/core/cells/cells.ts +29 -4
  63. package/src/core/cells/document-changes.ts +644 -0
  64. package/src/core/cells/ids.ts +5 -5
  65. package/src/core/cells/logs.ts +2 -2
  66. package/src/core/cells/runs.ts +6 -8
  67. package/src/core/codemirror/__tests__/format.test.ts +34 -36
  68. package/src/core/codemirror/__tests__/setup.test.ts +2 -2
  69. package/src/core/codemirror/cells/__tests__/extensions.test.ts +114 -0
  70. package/src/core/codemirror/cells/__tests__/traceback-decorations.test.ts +33 -32
  71. package/src/core/codemirror/cells/extensions.ts +66 -23
  72. package/src/core/codemirror/completion/__tests__/keymap.test.ts +15 -35
  73. package/src/core/codemirror/completion/keymap.ts +14 -4
  74. package/src/core/codemirror/copilot/__tests__/getCodes.test.ts +12 -13
  75. package/src/core/codemirror/language/__tests__/utils.test.ts +3 -3
  76. package/src/core/codemirror/language/embedded/__tests__/embedded-python.test.ts +7 -8
  77. package/src/core/codemirror/language/languages/python.ts +4 -0
  78. package/src/core/codemirror/lsp/__tests__/notebook-lsp.test.ts +4 -3
  79. package/src/core/codemirror/lsp/notebook-lsp.ts +28 -2
  80. package/src/core/codemirror/reactive-references/__tests__/analyzer.test.ts +7 -6
  81. package/src/core/codemirror/reactive-references/analyzer.ts +2 -2
  82. package/src/core/codemirror/rtc/loro/__tests__/sync.test.ts +52 -0
  83. package/src/core/codemirror/rtc/loro/sync.ts +1 -0
  84. package/src/core/datasets/__tests__/data-source.test.ts +5 -6
  85. package/src/core/datasets/state.ts +1 -1
  86. package/src/core/errors/__tests__/errors.test.ts +2 -1
  87. package/src/core/export/__tests__/hooks.test.ts +37 -36
  88. package/src/core/islands/bridge.ts +1 -0
  89. package/src/core/islands/main.ts +4 -7
  90. package/src/core/kernel/__tests__/handlers.test.ts +5 -4
  91. package/src/core/kernel/handlers.ts +7 -4
  92. package/src/core/network/DeferredRequestRegistry.ts +2 -2
  93. package/src/core/network/__tests__/CachingRequestRegistry.test.ts +9 -10
  94. package/src/core/network/__tests__/DeferredRequestRegistry.test.ts +4 -6
  95. package/src/core/network/requests-lazy.ts +1 -0
  96. package/src/core/network/requests-network.ts +9 -0
  97. package/src/core/network/requests-static.ts +1 -0
  98. package/src/core/network/requests-toasting.tsx +1 -0
  99. package/src/core/network/types.ts +5 -0
  100. package/src/core/static/__tests__/virtual-file-tracker.test.ts +8 -8
  101. package/src/core/static/virtual-file-tracker.ts +1 -1
  102. package/src/core/storage/__tests__/state.test.ts +31 -21
  103. package/src/core/storage/state.ts +1 -1
  104. package/src/core/variables/__tests__/state.test.ts +6 -6
  105. package/src/core/variables/types.ts +2 -2
  106. package/src/core/wasm/__tests__/state.test.ts +8 -8
  107. package/src/core/wasm/bridge.ts +1 -0
  108. package/src/core/websocket/useMarimoKernelConnection.tsx +31 -16
  109. package/src/css/app/fonts.css +6 -6
  110. package/src/css/md-tooltip.css +4 -39
  111. package/src/css/md.css +7 -0
  112. package/src/fonts/Fira_Mono/FiraMono-Bold.woff2 +0 -0
  113. package/src/fonts/Fira_Mono/FiraMono-Medium.woff2 +0 -0
  114. package/src/fonts/Fira_Mono/FiraMono-Regular.woff2 +0 -0
  115. package/src/fonts/Lora/Lora-VariableFont_wght.woff2 +0 -0
  116. package/src/fonts/PT_Sans/PTSans-Bold.woff2 +0 -0
  117. package/src/fonts/PT_Sans/PTSans-Regular.woff2 +0 -0
  118. package/src/plugins/core/RenderHTML.tsx +17 -0
  119. package/src/plugins/core/__test__/RenderHTML.test.ts +45 -0
  120. package/src/plugins/core/sanitize-html.ts +25 -18
  121. package/src/plugins/impl/DataTablePlugin.tsx +23 -2
  122. package/src/plugins/impl/SliderPlugin.tsx +1 -3
  123. package/src/plugins/impl/__tests__/SliderPlugin.test.tsx +120 -0
  124. package/src/plugins/impl/anywidget/model.ts +1 -2
  125. package/src/stories/cell.stories.tsx +8 -8
  126. package/src/stories/layout/vertical/one-column.stories.tsx +9 -8
  127. package/src/stories/log-viewer.stories.tsx +8 -8
  128. package/src/stories/variables.stories.tsx +2 -2
  129. package/src/utils/__tests__/download.test.tsx +21 -20
  130. package/src/utils/copy.ts +18 -5
  131. package/src/utils/createReducer.ts +26 -11
  132. package/src/utils/download.ts +4 -3
  133. package/src/utils/html-to-image.ts +6 -0
  134. package/src/utils/json/base64.ts +3 -3
  135. package/src/utils/traceback.ts +5 -3
  136. package/src/fonts/Fira_Mono/FiraMono-Bold.ttf +0 -0
  137. package/src/fonts/Fira_Mono/FiraMono-Medium.ttf +0 -0
  138. package/src/fonts/Fira_Mono/FiraMono-Regular.ttf +0 -0
  139. package/src/fonts/Lora/Lora-Italic-VariableFont_wght.ttf +0 -0
  140. package/src/fonts/Lora/Lora-VariableFont_wght.ttf +0 -0
  141. package/src/fonts/Lora/static/Lora-Bold.ttf +0 -0
  142. package/src/fonts/Lora/static/Lora-BoldItalic.ttf +0 -0
  143. package/src/fonts/Lora/static/Lora-Italic.ttf +0 -0
  144. package/src/fonts/Lora/static/Lora-Medium.ttf +0 -0
  145. package/src/fonts/Lora/static/Lora-MediumItalic.ttf +0 -0
  146. package/src/fonts/Lora/static/Lora-Regular.ttf +0 -0
  147. package/src/fonts/Lora/static/Lora-SemiBold.ttf +0 -0
  148. package/src/fonts/Lora/static/Lora-SemiBoldItalic.ttf +0 -0
  149. package/src/fonts/PT_Sans/PTSans-Bold.ttf +0 -0
  150. package/src/fonts/PT_Sans/PTSans-BoldItalic.ttf +0 -0
  151. package/src/fonts/PT_Sans/PTSans-Italic.ttf +0 -0
  152. package/src/fonts/PT_Sans/PTSans-Regular.ttf +0 -0
@@ -1,5 +1,6 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import { beforeEach, describe, expect, it } from "vitest";
3
+ import { variableName } from "@/__tests__/branded";
3
4
  import type { VariableName } from "../../variables/types";
4
5
  import { exportedForTesting } from "../state";
5
6
  import type { StorageEntry, StorageNamespace, StorageState } from "../types";
@@ -7,7 +8,7 @@ import type { StorageEntry, StorageNamespace, StorageState } from "../types";
7
8
  const { initialState, reducer, createActions } = exportedForTesting;
8
9
 
9
10
  function makeNamespace(
10
- overrides: Partial<StorageNamespace> & { name: string },
11
+ overrides: Partial<StorageNamespace> & { name: VariableName },
11
12
  ): StorageNamespace {
12
13
  return {
13
14
  backendType: overrides.backendType ?? "obstore",
@@ -53,8 +54,14 @@ describe("storage state", () => {
53
54
 
54
55
  describe("setNamespaces", () => {
55
56
  it("should add namespaces from an empty state", () => {
56
- const ns1 = makeNamespace({ name: "my_s3", protocol: "s3" });
57
- const ns2 = makeNamespace({ name: "my_gcs", protocol: "gcs" });
57
+ const ns1 = makeNamespace({
58
+ name: variableName("my_s3"),
59
+ protocol: "s3",
60
+ });
61
+ const ns2 = makeNamespace({
62
+ name: variableName("my_gcs"),
63
+ protocol: "gcs",
64
+ });
58
65
 
59
66
  actions.setNamespaces({ namespaces: [ns1, ns2] });
60
67
 
@@ -63,15 +70,18 @@ describe("storage state", () => {
63
70
 
64
71
  it("should merge namespaces by name, replacing existing ones", () => {
65
72
  const nsOld = makeNamespace({
66
- name: "my_s3",
73
+ name: variableName("my_s3"),
67
74
  protocol: "s3",
68
75
  rootPath: "/old",
69
76
  });
70
- const nsOther = makeNamespace({ name: "my_gcs", protocol: "gcs" });
77
+ const nsOther = makeNamespace({
78
+ name: variableName("my_gcs"),
79
+ protocol: "gcs",
80
+ });
71
81
  actions.setNamespaces({ namespaces: [nsOld, nsOther] });
72
82
 
73
83
  const nsUpdated = makeNamespace({
74
- name: "my_s3",
84
+ name: variableName("my_s3"),
75
85
  protocol: "s3",
76
86
  rootPath: "/new",
77
87
  });
@@ -82,10 +92,10 @@ describe("storage state", () => {
82
92
  });
83
93
 
84
94
  it("should add new namespaces alongside existing ones", () => {
85
- const ns1 = makeNamespace({ name: "ns1" });
95
+ const ns1 = makeNamespace({ name: variableName("ns1") });
86
96
  actions.setNamespaces({ namespaces: [ns1] });
87
97
 
88
- const ns2 = makeNamespace({ name: "ns2" });
98
+ const ns2 = makeNamespace({ name: variableName("ns2") });
89
99
  actions.setNamespaces({ namespaces: [ns2] });
90
100
 
91
101
  expect(state.namespaces).toHaveLength(2);
@@ -101,7 +111,7 @@ describe("storage state", () => {
101
111
  });
102
112
 
103
113
  actions.setNamespaces({
104
- namespaces: [makeNamespace({ name: "ns" })],
114
+ namespaces: [makeNamespace({ name: variableName("ns") })],
105
115
  });
106
116
 
107
117
  expect(state.entriesByPath.get("ns::")).toEqual([entry]);
@@ -205,7 +215,7 @@ describe("storage state", () => {
205
215
  });
206
216
 
207
217
  it("should not affect namespaces", () => {
208
- const ns = makeNamespace({ name: "ns" });
218
+ const ns = makeNamespace({ name: variableName("ns") });
209
219
  actions.setNamespaces({ namespaces: [ns] });
210
220
 
211
221
  actions.setEntries({
@@ -263,7 +273,7 @@ describe("storage state", () => {
263
273
  });
264
274
 
265
275
  it("should not affect namespaces", () => {
266
- const ns = makeNamespace({ name: "my_s3" });
276
+ const ns = makeNamespace({ name: variableName("my_s3") });
267
277
  actions.setNamespaces({ namespaces: [ns] });
268
278
  actions.setEntries({
269
279
  namespace: "my_s3",
@@ -293,31 +303,31 @@ describe("storage state", () => {
293
303
 
294
304
  describe("filterFromVariables", () => {
295
305
  it("should keep namespaces whose variable is still in scope", () => {
296
- const ns1 = makeNamespace({ name: "var_a" });
297
- const ns2 = makeNamespace({ name: "var_b" });
306
+ const ns1 = makeNamespace({ name: variableName("var_a") });
307
+ const ns2 = makeNamespace({ name: variableName("var_b") });
298
308
  actions.setNamespaces({ namespaces: [ns1, ns2] });
299
309
 
300
310
  actions.filterFromVariables([
301
- "var_a" as VariableName,
302
- "var_b" as VariableName,
311
+ variableName("var_a"),
312
+ variableName("var_b"),
303
313
  ]);
304
314
 
305
315
  expect(state.namespaces).toEqual([ns1, ns2]);
306
316
  });
307
317
 
308
318
  it("should remove namespaces whose variable is no longer in scope", () => {
309
- const ns1 = makeNamespace({ name: "var_a" });
310
- const ns2 = makeNamespace({ name: "var_b" });
319
+ const ns1 = makeNamespace({ name: variableName("var_a") });
320
+ const ns2 = makeNamespace({ name: variableName("var_b") });
311
321
  actions.setNamespaces({ namespaces: [ns1, ns2] });
312
322
 
313
- actions.filterFromVariables(["var_a" as VariableName]);
323
+ actions.filterFromVariables([variableName("var_a")]);
314
324
 
315
325
  expect(state.namespaces).toEqual([ns1]);
316
326
  });
317
327
 
318
328
  it("should remove all named namespaces when given an empty variable list", () => {
319
- const ns1 = makeNamespace({ name: "var_a" });
320
- const ns2 = makeNamespace({ name: "var_b" });
329
+ const ns1 = makeNamespace({ name: variableName("var_a") });
330
+ const ns2 = makeNamespace({ name: variableName("var_b") });
321
331
  actions.setNamespaces({ namespaces: [ns1, ns2] });
322
332
 
323
333
  actions.filterFromVariables([]);
@@ -326,7 +336,7 @@ describe("storage state", () => {
326
336
  });
327
337
 
328
338
  it("should not affect entriesByPath", () => {
329
- const ns = makeNamespace({ name: "ns" });
339
+ const ns = makeNamespace({ name: variableName("ns") });
330
340
  actions.setNamespaces({ namespaces: [ns] });
331
341
 
332
342
  const entry = makeEntry({ path: "file.txt" });
@@ -71,7 +71,7 @@ const {
71
71
  const names = new Set(variableNames);
72
72
  // Filter out namespaces whose backing variable is no longer in scope
73
73
  const namespaces = state.namespaces.filter((ns) => {
74
- return names.has(ns.name as VariableName);
74
+ return names.has(ns.name);
75
75
  });
76
76
  return { ...state, namespaces };
77
77
  },
@@ -1,19 +1,19 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import { beforeEach, describe, expect, it } from "vitest";
3
- import type { CellId } from "@/core/cells/ids";
3
+ import { cellId, variableName } from "@/__tests__/branded";
4
4
  import { exportedForTesting } from "../state";
5
- import type { VariableName, Variables } from "../types";
5
+ import type { Variables } from "../types";
6
6
 
7
7
  const { initialState, reducer, createActions } = exportedForTesting;
8
8
 
9
9
  const CellIds = {
10
- a: "a" as CellId,
11
- b: "b" as CellId,
10
+ a: cellId("a"),
11
+ b: cellId("b"),
12
12
  };
13
13
 
14
14
  const Names = {
15
- x: "x" as VariableName,
16
- y: "y" as VariableName,
15
+ x: variableName("x"),
16
+ y: variableName("y"),
17
17
  };
18
18
 
19
19
  describe("cell reducer", () => {
@@ -1,9 +1,9 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import type { TypedString } from "../../utils/typed";
3
+ import type { components } from "@marimo-team/marimo-api";
4
4
  import type { CellId } from "../cells/ids";
5
5
 
6
- export type VariableName = TypedString<"VariableName">;
6
+ export type VariableName = components["schemas"]["VariableName"];
7
7
 
8
8
  export interface Variable {
9
9
  name: VariableName;
@@ -2,9 +2,9 @@
2
2
 
3
3
  import { createRef } from "react";
4
4
  import { describe, expect, it } from "vitest";
5
+ import { cellId } from "@/__tests__/branded";
5
6
  import type { NotebookState } from "@/core/cells/cells";
6
7
  import { initialNotebookState, notebookAtom } from "@/core/cells/cells";
7
- import type { CellId } from "@/core/cells/ids";
8
8
  import { createCell, createCellRuntimeState } from "@/core/cells/types";
9
9
  import type { OutputMessage } from "@/core/kernel/messages";
10
10
  import { store } from "@/core/state/jotai";
@@ -17,17 +17,17 @@ describe("hasAnyOutputAtom", () => {
17
17
  ): NotebookState => ({
18
18
  ...initialNotebookState(),
19
19
  cellIds: new MultiColumn([
20
- CollapsibleTree.from(outputs.map((_, i) => `${i}` as CellId)),
20
+ CollapsibleTree.from(outputs.map((_, i) => cellId(`${i}`))),
21
21
  ]),
22
22
  cellData: Object.fromEntries(
23
23
  outputs.map((_, i) => [
24
- `${i}` as CellId,
25
- createCell({ id: `${i}` as CellId }),
24
+ cellId(`${i}`),
25
+ createCell({ id: cellId(`${i}`) }),
26
26
  ]),
27
27
  ),
28
28
  cellRuntime: Object.fromEntries(
29
29
  outputs.map((output, i) => [
30
- `${i}` as CellId,
30
+ `${i}`,
31
31
  createCellRuntimeState({
32
32
  output,
33
33
  status: "queued",
@@ -36,7 +36,7 @@ describe("hasAnyOutputAtom", () => {
36
36
  ]),
37
37
  ),
38
38
  cellHandles: Object.fromEntries(
39
- outputs.map((_, i) => [`${i}` as CellId, createRef()]),
39
+ outputs.map((_, i) => [cellId(`${i}`), createRef()]),
40
40
  ),
41
41
  });
42
42
 
@@ -90,8 +90,8 @@ describe("hasAnyOutputAtom", () => {
90
90
 
91
91
  it("should return true when all outputs are idle", () => {
92
92
  const notebookState = createNotebookState([null, null]);
93
- const cellId0 = "0" as CellId;
94
- const cellId1 = "1" as CellId;
93
+ const cellId0 = cellId("0");
94
+ const cellId1 = cellId("1");
95
95
  // Some idle cell, so returns false
96
96
  store.set(notebookAtom, {
97
97
  ...notebookState,
@@ -549,6 +549,7 @@ export class PyodideBridge implements RunRequests, EditRequests {
549
549
  };
550
550
 
551
551
  syncCellIds = () => Promise.resolve(null);
552
+ sendDocumentTransaction = () => Promise.resolve(null);
552
553
 
553
554
  addPackage: EditRequests["addPackage"] = async (request) => {
554
555
  return this.rpc.proxy.request.addPackage(request);
@@ -5,8 +5,12 @@ import { useRef } from "react";
5
5
  import { useErrorBoundary } from "react-error-boundary";
6
6
  import { toast } from "@/components/ui/use-toast";
7
7
  import { getNotebook, useCellActions } from "@/core/cells/cells";
8
+ import { applyTransactionChanges } from "@/core/cells/document-changes";
8
9
  import { AUTOCOMPLETER } from "@/core/codemirror/completion/Autocompleter";
9
- import type { NotificationPayload } from "@/core/kernel/messages";
10
+ import type {
11
+ NotificationMessageData,
12
+ NotificationPayload,
13
+ } from "@/core/kernel/messages";
10
14
  import { useConnectionTransport } from "@/core/websocket/useWebSocket";
11
15
  import { renderHTML } from "@/plugins/core/RenderHTML";
12
16
  import {
@@ -24,7 +28,7 @@ import { Logger } from "@/utils/Logger";
24
28
  import { reloadSafe } from "@/utils/reload-safe";
25
29
  import { useAlertActions } from "../alerts/state";
26
30
  import { cacheInfoAtom } from "../cache/requests";
27
- import { type CellId, SCRATCH_CELL_ID, type UIElementId } from "../cells/ids";
31
+ import { SCRATCH_CELL_ID } from "../cells/ids";
28
32
  import { useRunsActions } from "../cells/runs";
29
33
  import { focusAndScrollCellOutputIntoView } from "../cells/scrollCellIntoView";
30
34
  import type { CellData } from "../cells/types";
@@ -93,6 +97,17 @@ export function useMarimoKernelConnection(opts: {
93
97
  const { showBoundary } = useErrorBoundary();
94
98
 
95
99
  const { handleCellMessage, setCellCodes, setCellIds } = useCellActions();
100
+ const actionsWithoutMiddleware = useCellActions({ skipMiddleware: true });
101
+
102
+ const handleDocumentTransaction = (
103
+ transaction: NotificationMessageData<"notebook-document-transaction">["transaction"],
104
+ ) => {
105
+ applyTransactionChanges(
106
+ transaction.changes,
107
+ actionsWithoutMiddleware,
108
+ () => getNotebook().cellIds.inOrderIds,
109
+ );
110
+ };
96
111
  const { addCellNotification } = useRunsActions();
97
112
  const setKernelState = useSetAtom(kernelStateAtom);
98
113
  const setAppConfig = useSetAppConfig();
@@ -153,7 +168,7 @@ export function useMarimoKernelConnection(opts: {
153
168
  if (uiElement) {
154
169
  const buffers = safeExtractSetUIElementMessageBuffers(msg.data);
155
170
  UI_ELEMENT_REGISTRY.broadcastMessage(
156
- uiElement as UIElementId,
171
+ uiElement,
157
172
  msg.data.message,
158
173
  buffers,
159
174
  );
@@ -173,14 +188,11 @@ export function useMarimoKernelConnection(opts: {
173
188
  AUTOCOMPLETER.resolve(msg.data.completion_id as RequestId, msg.data);
174
189
  return;
175
190
  case "function-call-result":
176
- FUNCTIONS_REGISTRY.resolve(
177
- msg.data.function_call_id as RequestId,
178
- msg.data,
179
- );
191
+ FUNCTIONS_REGISTRY.resolve(msg.data.function_call_id, msg.data);
180
192
  return;
181
193
  case "cell-op": {
182
194
  handleCellNotificationeration(msg.data, handleCellMessage);
183
- const cellData = getNotebook().cellData[msg.data.cell_id as CellId];
195
+ const cellData = getNotebook().cellData[msg.data.cell_id];
184
196
  if (!cellData) {
185
197
  return;
186
198
  }
@@ -195,8 +207,8 @@ export function useMarimoKernelConnection(opts: {
195
207
  setVariables(
196
208
  msg.data.variables.map((v) => ({
197
209
  name: v.name as VariableName,
198
- declaredBy: v.declared_by as CellId[],
199
- usedBy: v.used_by as CellId[],
210
+ declaredBy: v.declared_by,
211
+ usedBy: v.used_by,
200
212
  })),
201
213
  );
202
214
  filterDatasetsFromVariables(
@@ -271,16 +283,16 @@ export function useMarimoKernelConnection(opts: {
271
283
  addColumnPreview(msg.data);
272
284
  return;
273
285
  case "sql-table-preview":
274
- PreviewSQLTable.resolve(msg.data.request_id as RequestId, msg.data);
286
+ PreviewSQLTable.resolve(msg.data.request_id, msg.data);
275
287
  return;
276
288
  case "sql-table-list-preview":
277
- PreviewSQLTableList.resolve(msg.data.request_id as RequestId, msg.data);
289
+ PreviewSQLTableList.resolve(msg.data.request_id, msg.data);
278
290
  return;
279
291
  case "validate-sql-result":
280
292
  ValidateSQL.resolve(msg.data.request_id as RequestId, msg.data);
281
293
  return;
282
294
  case "secret-keys-result":
283
- SECRETS_REGISTRY.resolve(msg.data.request_id as RequestId, msg.data);
295
+ SECRETS_REGISTRY.resolve(msg.data.request_id, msg.data);
284
296
  return;
285
297
  case "cache-info":
286
298
  setCacheInfo(msg.data);
@@ -310,19 +322,22 @@ export function useMarimoKernelConnection(opts: {
310
322
  return;
311
323
 
312
324
  case "focus-cell":
313
- focusAndScrollCellOutputIntoView(msg.data.cell_id as CellId);
325
+ focusAndScrollCellOutputIntoView(msg.data.cell_id);
314
326
  return;
315
327
  case "update-cell-codes":
316
328
  setCellCodes({
317
329
  codes: msg.data.codes,
318
- ids: msg.data.cell_ids as CellId[],
330
+ ids: msg.data.cell_ids,
319
331
  codeIsStale: msg.data.code_is_stale,
320
332
  names: msg.data.names,
321
333
  configs: msg.data.configs,
322
334
  });
323
335
  return;
324
336
  case "update-cell-ids":
325
- setCellIds({ cellIds: msg.data.cell_ids as CellId[] });
337
+ setCellIds({ cellIds: msg.data.cell_ids });
338
+ return;
339
+ case "notebook-document-transaction":
340
+ handleDocumentTransaction(msg.data.transaction);
326
341
  return;
327
342
  default:
328
343
  logNever(msg.data);
@@ -6,7 +6,7 @@ see frontend/src/core/static/download-html.ts */
6
6
  font-style: normal;
7
7
  font-weight: 400;
8
8
  font-display: block;
9
- src: url("../../fonts/Lora/Lora-VariableFont_wght.ttf") format("truetype");
9
+ src: url("../../fonts/Lora/Lora-VariableFont_wght.woff2") format("woff2");
10
10
  }
11
11
 
12
12
  @font-face {
@@ -14,7 +14,7 @@ see frontend/src/core/static/download-html.ts */
14
14
  font-style: normal;
15
15
  font-weight: 400;
16
16
  font-display: block;
17
- src: url("../../fonts/PT_Sans/PTSans-Regular.ttf") format("truetype");
17
+ src: url("../../fonts/PT_Sans/PTSans-Regular.woff2") format("woff2");
18
18
  }
19
19
 
20
20
  @font-face {
@@ -22,7 +22,7 @@ see frontend/src/core/static/download-html.ts */
22
22
  font-style: normal;
23
23
  font-weight: 700;
24
24
  font-display: block;
25
- src: url("../../fonts/PT_Sans/PTSans-Bold.ttf") format("truetype");
25
+ src: url("../../fonts/PT_Sans/PTSans-Bold.woff2") format("woff2");
26
26
  }
27
27
 
28
28
  @font-face {
@@ -30,7 +30,7 @@ see frontend/src/core/static/download-html.ts */
30
30
  font-style: normal;
31
31
  font-weight: 400;
32
32
  font-display: block;
33
- src: url("../../fonts/Fira_Mono/FiraMono-Regular.ttf") format("truetype");
33
+ src: url("../../fonts/Fira_Mono/FiraMono-Regular.woff2") format("woff2");
34
34
  }
35
35
 
36
36
  @font-face {
@@ -38,7 +38,7 @@ see frontend/src/core/static/download-html.ts */
38
38
  font-style: normal;
39
39
  font-weight: 500;
40
40
  font-display: block;
41
- src: url("../../fonts/Fira_Mono/FiraMono-Medium.ttf") format("truetype");
41
+ src: url("../../fonts/Fira_Mono/FiraMono-Medium.woff2") format("woff2");
42
42
  }
43
43
 
44
44
  @font-face {
@@ -46,5 +46,5 @@ see frontend/src/core/static/download-html.ts */
46
46
  font-style: normal;
47
47
  font-weight: 700;
48
48
  font-display: block;
49
- src: url("../../fonts/Fira_Mono/FiraMono-Bold.ttf") format("truetype");
49
+ src: url("../../fonts/Fira_Mono/FiraMono-Bold.woff2") format("woff2");
50
50
  }
@@ -3,50 +3,15 @@
3
3
  /*
4
4
  This allows you to create a basic tooltip using the data-tooltip attribute
5
5
  e.g. <span data-tooltip="Hello, World!">Hover me</span>
6
+
7
+ The tooltip content is rendered via the React Tooltip component (Radix UI portal),
8
+ which prevents clipping inside containers with overflow:hidden.
9
+ See: RenderHTML.tsx -> wrapTooltipTargets
6
10
  */
7
11
 
8
12
  .markdown {
9
13
  [data-tooltip] {
10
- position: relative;
11
14
  cursor: pointer;
12
15
  text-decoration: underline dotted;
13
16
  }
14
-
15
- [data-tooltip]::before,
16
- [data-tooltip]::after {
17
- visibility: hidden;
18
- opacity: 0;
19
- pointer-events: none;
20
- transition: all 0.2s ease;
21
- position: absolute;
22
- z-index: 1000;
23
- left: 50%;
24
- transform: translateX(-50%);
25
- }
26
-
27
- [data-tooltip]::before {
28
- content: attr(data-tooltip);
29
- bottom: calc(100% + 10px);
30
- padding: 5px 10px;
31
- width: max-content;
32
- max-width: 300px;
33
- border-radius: 6px;
34
- background-color: ;
35
- text-align: center;
36
- line-height: 1.4;
37
- white-space: pre-wrap;
38
-
39
- @apply bg-background text-foreground shadow-md border text-base;
40
- }
41
-
42
- [data-tooltip]:hover::before,
43
- [data-tooltip]:hover::after {
44
- visibility: visible;
45
- opacity: 1;
46
- }
47
-
48
- [data-tooltip]:hover::before {
49
- /* stylelint-disable-next-line unit-allowed-list */
50
- transform: translateX(-50%) translateY(10px);
51
- }
52
17
  }
package/src/css/md.css CHANGED
@@ -374,6 +374,13 @@ button .prose.prose {
374
374
  @apply p-4 pt-0;
375
375
  }
376
376
 
377
+ /* Restore proper list indentation inside details blocks.
378
+ The p-4 above overrides prose's padding-inline-start for bullet space.
379
+ This ensures bullets render correctly with list-style-position: outside. */
380
+ .markdown details > :is(ul, ol) {
381
+ padding-inline-start: 2.5rem;
382
+ }
383
+
377
384
  .markdown .codehilite {
378
385
  background-color: var(--slate-2);
379
386
  border-radius: 4px;
@@ -14,6 +14,7 @@ import React, {
14
14
  } from "react";
15
15
  import { CopyClipboardIcon } from "@/components/icons/copy-icon";
16
16
  import { QueryParamPreservingLink } from "@/components/ui/query-param-preserving-link";
17
+ import { Tooltip } from "@/components/ui/tooltip";
17
18
  import { DocHoverTarget } from "@/core/documentation/DocHoverTarget";
18
19
  import { sanitizeHtml, useSanitizeHtml } from "./sanitize";
19
20
 
@@ -160,6 +161,21 @@ const wrapDocHoverTargets: TransformFn = (
160
161
  }
161
162
  };
162
163
 
164
+ // Wrap elements with data-tooltip attribute in a Tooltip component.
165
+ // This renders the tooltip in a portal (top layer), fixing clipping inside
166
+ // containers with overflow:hidden (e.g. grid cells).
167
+ const wrapTooltipTargets: TransformFn = (
168
+ reactNode: ReactNode,
169
+ domNode: DOMNode,
170
+ ): JSX.Element | undefined => {
171
+ if (domNode instanceof Element && domNode.attribs?.["data-tooltip"]) {
172
+ const tooltipContent = domNode.attribs["data-tooltip"];
173
+ return (
174
+ <Tooltip content={tooltipContent}>{reactNode as JSX.Element}</Tooltip>
175
+ );
176
+ }
177
+ };
178
+
163
179
  const CopyableCode = ({ children }: { children: ReactNode }) => {
164
180
  const ref = useRef<HTMLDivElement>(null);
165
181
  return (
@@ -239,6 +255,7 @@ function parseHtml({
239
255
  addCopyButtonToCodehilite,
240
256
  preserveQueryParamsInAnchorLinks,
241
257
  wrapDocHoverTargets,
258
+ wrapTooltipTargets,
242
259
  removeWrappingBodyTags,
243
260
  removeWrappingHtmlTags,
244
261
  ];
@@ -197,6 +197,51 @@ describe("parseHtml", () => {
197
197
  });
198
198
  });
199
199
 
200
+ describe("wrapTooltipTargets", () => {
201
+ test("data-tooltip wraps element in Tooltip component", () => {
202
+ const html = '<span data-tooltip="Hello world">Hover me</span>';
203
+ expect(parseHtml({ html })).toMatchInlineSnapshot(`
204
+ <Tooltip
205
+ content="Hello world"
206
+ >
207
+ <span
208
+ data-tooltip="Hello world"
209
+ >
210
+ Hover me
211
+ </span>
212
+ </Tooltip>
213
+ `);
214
+ });
215
+
216
+ test("element without data-tooltip is not wrapped", () => {
217
+ const html = "<span>No tooltip</span>";
218
+ expect(parseHtml({ html })).toMatchInlineSnapshot(`
219
+ <span>
220
+ No tooltip
221
+ </span>
222
+ `);
223
+ });
224
+
225
+ test("data-tooltip on nested element wraps only that element", () => {
226
+ const html = '<p>Outer <span data-tooltip="tip">inner</span> text</p>';
227
+ expect(parseHtml({ html })).toMatchInlineSnapshot(`
228
+ <p>
229
+ Outer
230
+ <Tooltip
231
+ content="tip"
232
+ >
233
+ <span
234
+ data-tooltip="tip"
235
+ >
236
+ inner
237
+ </span>
238
+ </Tooltip>
239
+ text
240
+ </p>
241
+ `);
242
+ });
243
+ });
244
+
200
245
  describe("parseHtml with < nad >", () => {
201
246
  const html =
202
247
  'thread <unnamed> panicked at "assertion failed: `(left == right)`"';
@@ -2,28 +2,35 @@
2
2
  import DOMPurify, { type Config } from "dompurify";
3
3
 
4
4
  // preserve target=_blank https://github.com/cure53/DOMPurify/issues/317#issuecomment-912474068
5
- const TEMPORARY_ATTRIBUTE = "data-temp-href-target";
6
- DOMPurify.addHook("beforeSanitizeAttributes", (node) => {
7
- if (node.tagName === "A") {
8
- if (!node.hasAttribute("target")) {
9
- node.setAttribute("target", "_self");
10
- }
5
+ // Guard for non-browser environments (e.g. Node.js in the marimo-lsp extension)
6
+ // where `document` is not available.
7
+ if (typeof document !== "undefined") {
8
+ const TEMPORARY_ATTRIBUTE = "data-temp-href-target";
9
+ DOMPurify.addHook("beforeSanitizeAttributes", (node) => {
10
+ if (node.tagName === "A") {
11
+ if (!node.hasAttribute("target")) {
12
+ node.setAttribute("target", "_self");
13
+ }
11
14
 
12
- if (node.hasAttribute("target")) {
13
- node.setAttribute(TEMPORARY_ATTRIBUTE, node.getAttribute("target") || "");
15
+ if (node.hasAttribute("target")) {
16
+ node.setAttribute(
17
+ TEMPORARY_ATTRIBUTE,
18
+ node.getAttribute("target") || "",
19
+ );
20
+ }
14
21
  }
15
- }
16
- });
22
+ });
17
23
 
18
- DOMPurify.addHook("afterSanitizeAttributes", (node) => {
19
- if (node.tagName === "A" && node.hasAttribute(TEMPORARY_ATTRIBUTE)) {
20
- node.setAttribute("target", node.getAttribute(TEMPORARY_ATTRIBUTE) || "");
21
- node.removeAttribute(TEMPORARY_ATTRIBUTE);
22
- if (node.getAttribute("target") === "_blank") {
23
- node.setAttribute("rel", "noopener noreferrer");
24
+ DOMPurify.addHook("afterSanitizeAttributes", (node) => {
25
+ if (node.tagName === "A" && node.hasAttribute(TEMPORARY_ATTRIBUTE)) {
26
+ node.setAttribute("target", node.getAttribute(TEMPORARY_ATTRIBUTE) || "");
27
+ node.removeAttribute(TEMPORARY_ATTRIBUTE);
28
+ if (node.getAttribute("target") === "_blank") {
29
+ node.setAttribute("rel", "noopener noreferrer");
30
+ }
24
31
  }
25
- }
26
- });
32
+ });
33
+ }
27
34
 
28
35
  /**
29
36
  * This removes script tags, form tags, iframe tags, and other potentially dangerous tags