@marimo-team/islands 0.23.17-dev4 → 0.23.17-dev40

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 (185) hide show
  1. package/dist/{ConnectedDataExplorerComponent-f9vLjQiF.js → ConnectedDataExplorerComponent-BBZRR1-C.js} +16 -16
  2. package/dist/{ErrorBoundary-DhLqwSWA.js → ErrorBoundary-CptUP1_t.js} +3 -3
  3. package/dist/{ImageComparisonComponent-C3y7L3eJ.js → ImageComparisonComponent-fdY0LUuf.js} +38 -27
  4. package/dist/{any-language-editor-DifE-fl7.js → any-language-editor-BnIS9Mvg.js} +21 -21
  5. package/dist/assets/__vite-browser-external-DUz7vvG0.js +1 -0
  6. package/dist/assets/{worker-CDEKsBgX.js → worker-Cl1KJG2_.js} +2 -2
  7. package/dist/{button-BSm8IXRU.js → button-8aS1QcHr.js} +4 -2
  8. package/dist/{capabilities-C6OQ5YBO.js → capabilities-D23lgbH_.js} +1 -1
  9. package/dist/chat-ui-DH9M4L-6.js +8620 -0
  10. package/dist/{check-DP7U0H0G.js → check-D-BaEjEh.js} +1 -1
  11. package/dist/{common-BcQ4kHDP.js → common-DSVkRiDF.js} +652 -652
  12. package/dist/{copy-DNq8q3zw.js → copy-BkA3t7rJ.js} +2 -2
  13. package/dist/{defaultLocale-BpsHxBd7.js → defaultLocale-BsfI21d_.js} +26 -26
  14. package/dist/{diagram-PSM6KHXK-D4ftnaua.js → diagram-PSM6KHXK-D3GsWNcQ.js} +1 -1
  15. package/dist/{dist-BpeiYpG2.js → dist-3F1XDSYc.js} +2 -2
  16. package/dist/{dist-BP39XM8u.js → dist-B-upo54Y.js} +1 -1
  17. package/dist/{dist-BdqCKQ1O.js → dist-B1W12VzQ.js} +2 -2
  18. package/dist/{dist-3NCqaWjt.js → dist-B6au1PTB.js} +2 -2
  19. package/dist/{dist-BCX980mU.js → dist-BDwn6WIk.js} +2 -2
  20. package/dist/dist-BDzVdDo2.js +8 -0
  21. package/dist/dist-BH2Tq1S1.js +5 -0
  22. package/dist/dist-BIhBrzqg.js +5 -0
  23. package/dist/dist-BLCsvXTG.js +6 -0
  24. package/dist/dist-BNVtuFdd.js +5 -0
  25. package/dist/{dist-BOMbzyb3.js → dist-BNeJ8P-4.js} +3 -3
  26. package/dist/{dist-LpLXa9Lb.js → dist-BT96P4oR.js} +2 -2
  27. package/dist/{dist-CFSb_Frm.js → dist-BUzxp0Xf.js} +1 -1
  28. package/dist/{dist-JGehap7K.js → dist-Ba1WFESi.js} +5 -5
  29. package/dist/{dist-CRmSjkLP.js → dist-BrKNRKC8.js} +2 -2
  30. package/dist/{dist-BIEt-mlF.js → dist-BvvQUFUK.js} +1 -1
  31. package/dist/{dist-pqIwD8VD.js → dist-BxR-g1ig.js} +2 -2
  32. package/dist/{dist-DviUBLUp.js → dist-CBraAJlT.js} +4 -4
  33. package/dist/{dist-BwhXBwMd.js → dist-CDBV8n0I.js} +1 -1
  34. package/dist/dist-CEJ6Lowi.js +5 -0
  35. package/dist/{dist-CSlJrrAw.js → dist-CWYsUX_O.js} +1 -1
  36. package/dist/dist-CYoa69kj.js +5 -0
  37. package/dist/{dist-DwqBJ014.js → dist-CdKRSqjo.js} +2 -2
  38. package/dist/{dist-De-GlSAs.js → dist-Cds3J-EE.js} +15 -9
  39. package/dist/dist-CnZyXFq3.js +8 -0
  40. package/dist/dist-D-4HaJKc.js +8 -0
  41. package/dist/{dist-C18MUWTj.js → dist-D8SX5Qdg.js} +2 -2
  42. package/dist/{dist-D3bdsa1d.js → dist-DFQ91Mx1.js} +4 -4
  43. package/dist/{dist-BPF0gTRZ.js → dist-DGQmYLkh.js} +1 -1
  44. package/dist/dist-DIGyvbg3.js +5 -0
  45. package/dist/dist-DIhaoCL4.js +5 -0
  46. package/dist/{dist-C2_4pjuS.js → dist-DK0pFEmc.js} +1 -1
  47. package/dist/{dist-BTdrwBWP.js → dist-DQj6A7SE.js} +3 -3
  48. package/dist/{dist-DIlxLFXG.js → dist-DarwE0ZO.js} +11 -11
  49. package/dist/{dist-DaXJPX8F.js → dist-Ddb0VFS3.js} +2 -2
  50. package/dist/dist-DgQleHmb.js +8 -0
  51. package/dist/{dist-DZwp8lro.js → dist-DgkJegw4.js} +3 -3
  52. package/dist/{dist-BO5HRiTf.js → dist-DjQRRttD.js} +2 -2
  53. package/dist/dist-JaoLzZmt.js +5 -0
  54. package/dist/{dist-B2jmQKKx.js → dist-PQDZaPEo.js} +2 -2
  55. package/dist/{dist-4lVIWUT3.js → dist-Slyx4vXr.js} +1 -1
  56. package/dist/{dist-DgjbnqTg.js → dist-dJeh9ef4.js} +2 -2
  57. package/dist/dist-pEX0t8ZV.js +6 -0
  58. package/dist/{dist-w_gWE5Wf.js → dist-v-kAJfMq.js} +1 -1
  59. package/dist/{error-banner-B_GQfxxD.js → error-banner-dmkbm21G.js} +2 -2
  60. package/dist/{esm-DzZVaNH4.js → esm-D8vjxCyn.js} +3 -3
  61. package/dist/{esm-DKOv6P2M.js → esm-DEQU-LUO.js} +22 -22
  62. package/dist/{extends-w2_S44bV.js → extends-DDOVg_b8.js} +5 -5
  63. package/dist/{formats-Dwq1THhr.js → formats-xc-Msa0v.js} +3 -3
  64. package/dist/{ganttDiagram-JELNMOA3-CXXCN7LN.js → ganttDiagram-JELNMOA3-D9ywbC5g.js} +3 -3
  65. package/dist/{glide-data-editor-B9K6-MJe.js → glide-data-editor-CyTNN635.js} +9 -9
  66. package/dist/{html-to-image-CZ1kLKkq.js → html-to-image-DrEMf75G.js} +2219 -2209
  67. package/dist/{input-C5K4VBvB.js → input-B4ddy8Jy.js} +6 -6
  68. package/dist/{label-DjoYwqmj.js → label-CzHncDvS.js} +2 -2
  69. package/dist/{linear-H_vsH9ao.js → linear-BOGUO8Cd.js} +2 -2
  70. package/dist/{loader-DX-IDS7n.js → loader-C7SfUdOD.js} +10 -10
  71. package/dist/main.js +729 -733
  72. package/dist/{mermaid-hv6IckVw.js → mermaid-CpL8y5pw.js} +9 -9
  73. package/dist/{precisionRound-CnHPY_5v.js → precisionRound-Bq50n-bG.js} +1 -1
  74. package/dist/{process-output-bVfbcx5_.js → process-output-B9IHQuus.js} +3 -3
  75. package/dist/{quadrantDiagram-AYHSOK5B-BqDB9tUC.js → quadrantDiagram-AYHSOK5B-DD4QBdSv.js} +2 -2
  76. package/dist/{react-vega-BazDALhI.js → react-vega-Cg17A6rO.js} +8017 -4777
  77. package/dist/{reveal-component-9QGZq7uF.js → reveal-component-CUzZw4ZH.js} +19 -19
  78. package/dist/{spec-CQKL_D3i.js → spec-uMKNvMRZ.js} +4 -4
  79. package/dist/{strings-DIy9NAmg.js → strings-BUDhKc0s.js} +299 -299
  80. package/dist/style.css +1 -1
  81. package/dist/{swiper-component-CEdAYjb7.js → swiper-component-dl1H9WIW.js} +2 -2
  82. package/dist/{time-Bb4A7I6a.js → time-CQnohwNM.js} +2 -2
  83. package/dist/{toDate-CqhBWjvO.js → toDate-BiyuFdnT.js} +3 -3
  84. package/dist/{tooltip-f-qR0iUx.js → tooltip-CkCWlUOf.js} +3 -3
  85. package/dist/{types-DicH9Oi0.js → types-uENjXsiC.js} +1 -1
  86. package/dist/{useAsyncData-CYOQb1EW.js → useAsyncData-DqJLysjC.js} +1 -1
  87. package/dist/{useDateFormatter-Bt2qr_XS.js → useDateFormatter-BeN3fHMH.js} +2 -2
  88. package/dist/{useDeepCompareMemoize-CSHQeQEI.js → useDeepCompareMemoize-CvlmHAmy.js} +1 -1
  89. package/dist/{useIframeCapabilities-kA09ykNq.js → useIframeCapabilities-pLC-ceDP.js} +1 -1
  90. package/dist/{useLifecycle-CMJGqBaY.js → useLifecycle-D4N02q82.js} +3 -3
  91. package/dist/{useTheme-DYfby65G.js → useTheme-Dur2p3iC.js} +4 -3
  92. package/dist/{vega-component-C7nO5zcf.js → vega-component-Cft458Sr.js} +13 -13
  93. package/dist/{vega-embed-container-DAQAMjiO.js → vega-embed-container-S9gRu4TZ.js} +3 -3
  94. package/dist/{vega-loader.browser-DmpxCJqx.js → vega-loader.browser-CmLRI3wk.js} +104 -111
  95. package/dist/{xychartDiagram-PRI3JC2R-BSkNxYNw.js → xychartDiagram-PRI3JC2R-D9DgEtKn.js} +2 -2
  96. package/dist/{zod-BUcyXSdR.js → zod-yTHHzJiv.js} +37 -38
  97. package/package.json +6 -6
  98. package/src/__mocks__/requests.ts +5 -0
  99. package/src/components/app-config/user-config-form.tsx +26 -0
  100. package/src/components/chat/__tests__/chat-abort.test.ts +51 -0
  101. package/src/components/chat/chat-abort.ts +44 -0
  102. package/src/components/chat/chat-display.tsx +17 -0
  103. package/src/components/chat/chat-panel.tsx +85 -37
  104. package/src/components/chat/chat-utils.ts +1 -1
  105. package/src/components/data-table/charts/__tests__/schemas.test.ts +35 -0
  106. package/src/components/data-table/charts/schemas.ts +5 -2
  107. package/src/components/databases/icons/huggingface.svg +8 -0
  108. package/src/components/datasets/__tests__/icons.test.ts +21 -0
  109. package/src/components/datasets/icons.tsx +10 -0
  110. package/src/components/datasources/__tests__/components.test.tsx +29 -0
  111. package/src/components/datasources/components.tsx +8 -3
  112. package/src/components/editor/actions/export-dialog/__tests__/export-dialog.test.tsx +35 -0
  113. package/src/components/editor/actions/export-dialog/__tests__/state.test.ts +61 -0
  114. package/src/components/editor/actions/export-dialog/export-command.ts +4 -9
  115. package/src/components/editor/actions/export-dialog/export-dialog.tsx +27 -23
  116. package/src/components/editor/actions/export-dialog/format-notice.tsx +28 -0
  117. package/src/components/editor/actions/export-dialog/state.ts +23 -5
  118. package/src/components/editor/actions/export-dialog/use-export-dialog.ts +7 -15
  119. package/src/components/editor/actions/useConfigActions.tsx +24 -0
  120. package/src/components/editor/ai/add-cell-with-ai.tsx +2 -2
  121. package/src/components/editor/ai/ai-completion-editor.tsx +2 -2
  122. package/src/components/editor/chrome/panels/outline/__tests__/useActiveOutline.test.ts +26 -0
  123. package/src/components/editor/code/__tests__/readonly-python-code.test.tsx +114 -2
  124. package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +14 -0
  125. package/src/components/editor/connections/storage/__tests__/as-code.test.ts +20 -0
  126. package/src/components/editor/connections/storage/add-storage-form.tsx +10 -0
  127. package/src/components/editor/connections/storage/as-code.ts +19 -1
  128. package/src/components/editor/connections/storage/schemas.ts +19 -0
  129. package/src/components/storage/__tests__/storage-snippets.test.ts +88 -0
  130. package/src/components/storage/components.tsx +2 -0
  131. package/src/components/storage/storage-snippets.ts +58 -0
  132. package/src/core/__tests__/mode.test.ts +85 -0
  133. package/src/core/ai/constants.ts +7 -0
  134. package/src/core/ai/staged-cells.ts +3 -0
  135. package/src/core/codemirror/__tests__/setup.test.ts +1 -1
  136. package/src/core/codemirror/cm.ts +2 -2
  137. package/src/core/codemirror/code-lens/__tests__/extension.test.ts +4 -5
  138. package/src/core/codemirror/code-lens/extension.ts +3 -3
  139. package/src/core/codemirror/find-replace/search-highlight.ts +8 -3
  140. package/src/core/codemirror/language/languages/sql/__tests__/renderers.test.tsx +44 -0
  141. package/src/core/codemirror/language/languages/sql/renderers.tsx +3 -3
  142. package/src/core/codemirror/theme/__tests__/light.test.ts +59 -123
  143. package/src/core/codemirror/theme/dark.ts +6 -5
  144. package/src/core/codemirror/theme/light.ts +6 -4
  145. package/src/core/config/__tests__/config-schema.test.ts +2 -0
  146. package/src/core/config/config-schema.ts +1 -0
  147. package/src/core/config/feature-flag.tsx +0 -2
  148. package/src/core/dom/outline.ts +25 -2
  149. package/src/core/hotkeys/hotkeys.ts +2 -0
  150. package/src/core/mode.ts +11 -9
  151. package/src/core/network/__tests__/api.test.ts +132 -17
  152. package/src/core/network/__tests__/export-filename.test.ts +43 -0
  153. package/src/core/network/__tests__/requests-lazy.test.ts +1 -0
  154. package/src/core/network/api.ts +47 -12
  155. package/src/core/network/export-filename.ts +37 -0
  156. package/src/core/network/markdown-export.ts +19 -0
  157. package/src/core/network/requests-network.ts +29 -5
  158. package/src/core/network/types.ts +3 -0
  159. package/src/core/storage/types.ts +1 -0
  160. package/src/css/app/codemirror.css +11 -7
  161. package/src/css/globals.css +8 -0
  162. package/src/plugins/impl/DataEditorPlugin.tsx +2 -9
  163. package/src/plugins/impl/__tests__/DataEditorPlugin.test.ts +18 -0
  164. package/src/plugins/impl/chat/chat-ui.tsx +51 -3
  165. package/src/plugins/impl/data-frames/__tests__/schema.test.ts +30 -0
  166. package/src/plugins/impl/data-frames/schema.ts +4 -1
  167. package/src/plugins/impl/image-comparison/ImageComparisonComponent.tsx +20 -1
  168. package/src/plugins/impl/image-comparison/__tests__/ImageComparisonComponent.test.tsx +58 -0
  169. package/src/stories/theme.stories.tsx +71 -1
  170. package/dist/assets/__vite-browser-external-DltVXDv7.js +0 -1
  171. package/dist/chat-ui-DsZJj75A.js +0 -8369
  172. package/dist/dist-5o9uz19x.js +0 -5
  173. package/dist/dist-BpDH0DEP.js +0 -5
  174. package/dist/dist-BpXhbQXO.js +0 -8
  175. package/dist/dist-ByadAk_L.js +0 -5
  176. package/dist/dist-CK0g7JvP.js +0 -8
  177. package/dist/dist-CfzCtiq2.js +0 -5
  178. package/dist/dist-D8JtVFrW.js +0 -8
  179. package/dist/dist-DK2Oocpu.js +0 -6
  180. package/dist/dist-DnSu977r.js +0 -5
  181. package/dist/dist-UmxJm6sW.js +0 -8
  182. package/dist/dist-aYSBQSyM.js +0 -5
  183. package/dist/dist-kkZ1Ztlh.js +0 -6
  184. package/dist/dist-ngPczwNi.js +0 -5
  185. package/dist/dist-wQRyxyTV.js +0 -5
@@ -0,0 +1,29 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { render } from "@testing-library/react";
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import type { DataType } from "@/core/kernel/messages";
6
+ import { Logger } from "@/utils/Logger";
7
+ import { ColumnName } from "../components";
8
+
9
+ describe("ColumnName", () => {
10
+ it("renders the unknown icon for unrecognized data types", () => {
11
+ const warn = vi.spyOn(Logger, "warn").mockImplementation(() => {});
12
+
13
+ try {
14
+ const unrecognized = render(
15
+ <ColumnName columnName="geom" dataType={"geometry" as DataType} />,
16
+ );
17
+ const unknown = render(
18
+ <ColumnName columnName="geom" dataType="unknown" />,
19
+ );
20
+
21
+ expect(unrecognized.container.innerHTML).toEqual(
22
+ unknown.container.innerHTML,
23
+ );
24
+ expect(warn).not.toHaveBeenCalled();
25
+ } finally {
26
+ warn.mockRestore();
27
+ }
28
+ });
29
+ });
@@ -5,7 +5,11 @@ import type { CSSProperties } from "react";
5
5
  import { TreeChevron } from "@/components/editor/file-tree/tree-actions";
6
6
  import type { DataType } from "@/core/kernel/messages";
7
7
  import { cn } from "@/utils/cn";
8
- import { DATA_TYPE_ICON, getDataTypeColor } from "../datasets/icons";
8
+ import {
9
+ DATA_TYPE_ICON,
10
+ getDataTypeColor,
11
+ resolveDataType,
12
+ } from "../datasets/icons";
9
13
 
10
14
  export const RotatingChevron: React.FC<{ isExpanded: boolean }> = ({
11
15
  isExpanded,
@@ -101,8 +105,9 @@ export const ColumnName = ({
101
105
  columnName: React.ReactNode;
102
106
  dataType: DataType;
103
107
  }) => {
104
- const Icon = DATA_TYPE_ICON[dataType];
105
- const color = getDataTypeColor(dataType);
108
+ const resolvedDataType = resolveDataType(dataType);
109
+ const Icon = DATA_TYPE_ICON[resolvedDataType];
110
+ const color = getDataTypeColor(resolvedDataType);
106
111
 
107
112
  return (
108
113
  <div className="flex flex-row items-center gap-1.5">
@@ -259,6 +259,7 @@ describe("ExportDialog", () => {
259
259
  format: "pdf",
260
260
  dependenciesAvailable: false,
261
261
  missingPackages: ["nbconvert[webpdf]"],
262
+ missingSetup: [],
262
263
  },
263
264
  ],
264
265
  }),
@@ -275,6 +276,40 @@ describe("ExportDialog", () => {
275
276
  expect(screen.getByTestId("export-format-pdf")).toBeVisible();
276
277
  });
277
278
 
279
+ it("explains missing Playwright Chromium setup", async () => {
280
+ store.set(
281
+ requestClientAtom,
282
+ MockRequestClient.create({
283
+ getExportAvailability: vi.fn().mockResolvedValue({
284
+ source: "server",
285
+ formats: [
286
+ {
287
+ format: "pdf",
288
+ dependenciesAvailable: false,
289
+ missingPackages: [],
290
+ missingSetup: [
291
+ {
292
+ name: "playwright-chromium",
293
+ command: "uv run playwright install chromium",
294
+ },
295
+ ],
296
+ },
297
+ ],
298
+ }),
299
+ }),
300
+ );
301
+
302
+ renderDialog("pdf");
303
+
304
+ expect(
305
+ await screen.findByText("PDF export requires Playwright Chromium."),
306
+ ).toBeVisible();
307
+ expect(
308
+ screen.getByText("uv run playwright install chromium"),
309
+ ).toBeVisible();
310
+ expect(screen.getByTestId("export-submit")).toBeDisabled();
311
+ });
312
+
278
313
  it("announces requirement checks as status updates", () => {
279
314
  store.set(
280
315
  requestClientAtom,
@@ -19,11 +19,13 @@ const AVAILABLE: ExportAvailabilityResponse = {
19
19
  format: "ipynb",
20
20
  dependenciesAvailable: true,
21
21
  missingPackages: [],
22
+ missingSetup: [],
22
23
  },
23
24
  {
24
25
  format: "pdf",
25
26
  dependenciesAvailable: true,
26
27
  missingPackages: [],
28
+ missingSetup: [],
27
29
  },
28
30
  ],
29
31
  };
@@ -188,6 +190,7 @@ describe("getExportFormatStatus", () => {
188
190
  format: "pdf",
189
191
  dependenciesAvailable: false,
190
192
  missingPackages: ["nbconvert[webpdf]"],
193
+ missingSetup: [],
191
194
  },
192
195
  ],
193
196
  },
@@ -202,6 +205,64 @@ describe("getExportFormatStatus", () => {
202
205
  });
203
206
  });
204
207
 
208
+ it("reports missing server setup", () => {
209
+ expect(
210
+ status("pdf", {
211
+ availability: {
212
+ status: "success",
213
+ data: {
214
+ source: "server",
215
+ formats: [
216
+ {
217
+ format: "pdf",
218
+ dependenciesAvailable: false,
219
+ missingPackages: [],
220
+ missingSetup: [
221
+ {
222
+ name: "playwright-chromium",
223
+ command: "uv run playwright install chromium",
224
+ },
225
+ ],
226
+ },
227
+ ],
228
+ },
229
+ },
230
+ }),
231
+ ).toEqual({
232
+ available: false,
233
+ reason: {
234
+ type: "missing-setup",
235
+ requirements: [
236
+ {
237
+ name: "playwright-chromium",
238
+ command: "uv run playwright install chromium",
239
+ },
240
+ ],
241
+ },
242
+ });
243
+ });
244
+
245
+ it("fails open when an unavailable format has no requirements", () => {
246
+ expect(
247
+ status("pdf", {
248
+ availability: {
249
+ status: "success",
250
+ data: {
251
+ source: "server",
252
+ formats: [
253
+ {
254
+ format: "pdf",
255
+ dependenciesAvailable: false,
256
+ missingPackages: [],
257
+ missingSetup: [],
258
+ },
259
+ ],
260
+ },
261
+ },
262
+ }),
263
+ ).toEqual({ available: true, availabilityCheckFailed: true });
264
+ });
265
+
205
266
  it.each([
206
267
  ["html", true, undefined],
207
268
  ["markdown", true, undefined],
@@ -1,18 +1,13 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
+ import {
4
+ MARKDOWN_EXTENSIONS,
5
+ MARKDOWN_SUFFIXES,
6
+ } from "@/core/network/markdown-export";
3
7
  import { Filenames } from "@/utils/filenames";
4
8
  import { shellQuote } from "@/utils/shell";
5
9
  import type { ExportFormat, ExportOptions, MarkdownFlavor } from "./state";
6
10
 
7
- const MARKDOWN_EXTENSIONS: Record<MarkdownFlavor, string> = {
8
- pymdown: "md",
9
- qmd: "qmd",
10
- mystmd: "myst.md",
11
- mdx: "mdx",
12
- };
13
-
14
- const MARKDOWN_SUFFIXES = [".myst.md", ".markdown", ".qmd", ".mdx", ".md"];
15
-
16
11
  function inferMarkdownFlavor(filename: string): MarkdownFlavor {
17
12
  if (filename.endsWith(".myst.md")) {
18
13
  return "mystmd";
@@ -101,31 +101,35 @@ export const ExportDialog: React.FC<{
101
101
  })}
102
102
  </TabsList>
103
103
 
104
- <TabsContent
105
- value={format}
106
- className="mt-0 min-h-0 min-w-0 overflow-y-auto px-4 py-3"
107
- >
108
- <div className="mb-3">
109
- <h3 className="text-base font-semibold">{definition.label}</h3>
110
- <p className="mt-0.5 text-sm text-muted-foreground">
111
- {definition.description}
112
- </p>
113
- </div>
114
-
115
- <FormatNotice
116
- format={format}
117
- formatLabel={definition.label}
118
- status={status}
119
- />
104
+ {/* One panel per tab so each trigger's aria-controls resolves. */}
105
+ {formats.map(({ format: candidate }) => (
106
+ <TabsContent
107
+ key={candidate}
108
+ value={candidate}
109
+ className="mt-0 min-h-0 min-w-0 overflow-y-auto px-4 py-3"
110
+ >
111
+ <div className="mb-3">
112
+ <h3 className="text-base font-semibold">{definition.label}</h3>
113
+ <p className="mt-0.5 text-sm text-muted-foreground">
114
+ {definition.description}
115
+ </p>
116
+ </div>
120
117
 
121
- {usesBrowserPrint ? null : (
122
- <FormatOptions
123
- options={options}
124
- updateOptions={updateOptions}
125
- disabled={isExporting}
118
+ <FormatNotice
119
+ format={format}
120
+ formatLabel={definition.label}
121
+ status={status}
126
122
  />
127
- )}
128
- </TabsContent>
123
+
124
+ {usesBrowserPrint ? null : (
125
+ <FormatOptions
126
+ options={options}
127
+ updateOptions={updateOptions}
128
+ disabled={isExporting}
129
+ />
130
+ )}
131
+ </TabsContent>
132
+ ))}
129
133
  </Tabs>
130
134
 
131
135
  <footer className="min-w-0 border-t bg-muted/20 px-4 py-3">
@@ -10,8 +10,18 @@ import type {
10
10
  ExportBlockReason,
11
11
  ExportFormat,
12
12
  ExportFormatStatus,
13
+ ExportSetupRequirement,
13
14
  } from "./state";
14
15
 
16
+ const SETUP_REQUIREMENTS: Record<
17
+ ExportSetupRequirement["name"],
18
+ { description: string }
19
+ > = {
20
+ "playwright-chromium": {
21
+ description: "PDF export requires Playwright Chromium.",
22
+ },
23
+ };
24
+
15
25
  export function FormatStatusIcon({ status }: { status: ExportFormatStatus }) {
16
26
  if (status.available && !status.availabilityCheckFailed) {
17
27
  return null;
@@ -124,6 +134,24 @@ function ReasonNotice({
124
134
  where marimo is running to use this export.
125
135
  </Notice>
126
136
  );
137
+ case "missing-setup":
138
+ return (
139
+ <Notice>
140
+ <span className="block min-w-0">
141
+ {reason.requirements.map((requirement) => {
142
+ const details = SETUP_REQUIREMENTS[requirement.name];
143
+ return (
144
+ <span className="block" key={requirement.name}>
145
+ {details.description}
146
+ <code className="mt-1 block break-all font-mono">
147
+ {requirement.command}
148
+ </code>
149
+ </span>
150
+ );
151
+ })}
152
+ </span>
153
+ </Notice>
154
+ );
127
155
  case "wasm-runtime":
128
156
  return (
129
157
  <Notice>
@@ -15,6 +15,8 @@ export const EXPORT_FORMATS = [
15
15
  ] as const;
16
16
 
17
17
  export type ExportFormat = (typeof EXPORT_FORMATS)[number];
18
+ export type ExportSetupRequirement =
19
+ ExportAvailabilityResponse["formats"][number]["missingSetup"][number];
18
20
 
19
21
  export const MARKDOWN_FLAVORS = ["pymdown", "qmd", "mystmd", "mdx"] as const;
20
22
  export const IPYNB_SORT_MODES = ["topological", "top-down"] as const;
@@ -185,6 +187,7 @@ export type ExportBlockReason =
185
187
  | { type: "checking-requirements" }
186
188
  | { type: "notebook-must-be-named" }
187
189
  | { type: "missing-packages"; packages: string[] }
190
+ | { type: "missing-setup"; requirements: ExportSetupRequirement[] }
188
191
  | { type: "wasm-runtime" };
189
192
 
190
193
  export interface ExportFormatStatus {
@@ -308,12 +311,27 @@ export function getExportFormatStatus({
308
311
  return { available: true, availabilityCheckFailed: true };
309
312
  }
310
313
  if (!serverStatus.dependenciesAvailable) {
314
+ if (serverStatus.missingPackages.length > 0) {
315
+ return {
316
+ available: false,
317
+ reason: {
318
+ type: "missing-packages",
319
+ packages: serverStatus.missingPackages,
320
+ },
321
+ };
322
+ }
323
+ if (serverStatus.missingSetup.length > 0) {
324
+ return {
325
+ available: false,
326
+ reason: {
327
+ type: "missing-setup",
328
+ requirements: serverStatus.missingSetup,
329
+ },
330
+ };
331
+ }
311
332
  return {
312
- available: false,
313
- reason: {
314
- type: "missing-packages",
315
- packages: serverStatus.missingPackages,
316
- },
333
+ available: true,
334
+ availabilityCheckFailed: true,
317
335
  };
318
336
  }
319
337
 
@@ -1,13 +1,13 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { useAtom, useAtomValue, useSetAtom } from "jotai";
3
+ import { useAtom } from "jotai";
4
4
  import { useEffect, useRef, useState } from "react";
5
5
  import { toast } from "@/components/ui/use-toast";
6
6
  import {
7
7
  updateCellOutputsWithScreenshots,
8
8
  useEnrichCellOutputs,
9
9
  } from "@/core/export/hooks";
10
- import { runDuringPresentMode, viewStateAtom } from "@/core/mode";
10
+ import { runDuringPresentMode } from "@/core/mode";
11
11
  import { useRequestClient } from "@/core/network/requests";
12
12
  import type { ExportAvailabilityResponse } from "@/core/network/types";
13
13
  import { useFilename } from "@/core/saving/filename";
@@ -21,6 +21,7 @@ import {
21
21
  withLoadingToast,
22
22
  } from "@/utils/download";
23
23
  import { Filenames } from "@/utils/filenames";
24
+ import { Logger } from "@/utils/Logger";
24
25
  import { Paths } from "@/utils/paths";
25
26
  import { getExportCommand } from "./export-command";
26
27
  import { exportNotebook } from "./export-notebook";
@@ -277,8 +278,6 @@ function useExportDialogAction({
277
278
  }) {
278
279
  const requests = useRequestClient();
279
280
  const takeScreenshots = useEnrichCellOutputs();
280
- const viewState = useAtomValue(viewStateAtom);
281
- const setViewState = useSetAtom(viewStateAtom);
282
281
  const [isExporting, setIsExporting] = useState(false);
283
282
  const dialogRef = useRef<HTMLDivElement>(null);
284
283
  const mountedRef = useRef(true);
@@ -302,15 +301,7 @@ function useExportDialogAction({
302
301
  const capturePNG = async () => {
303
302
  const capture = () =>
304
303
  captureCurrentAppView(dialogRef.current?.parentElement ?? null);
305
- if (viewState.mode !== "edit") {
306
- await capture();
307
- return;
308
- }
309
- try {
310
- await runDuringPresentMode(capture);
311
- } finally {
312
- setViewState(viewState);
313
- }
304
+ await runDuringPresentMode(capture);
314
305
  };
315
306
 
316
307
  const submit = async () => {
@@ -342,8 +333,9 @@ function useExportDialogAction({
342
333
  if (mountedRef.current) {
343
334
  onClose();
344
335
  }
345
- } catch {
346
- // Request and capture helpers report actionable errors to the user.
336
+ } catch (error) {
337
+ // Most helpers toast actionable errors, but not all (e.g. updateCellOutputs).
338
+ Logger.error("Export failed", error);
347
339
  } finally {
348
340
  if (mountedRef.current) {
349
341
  setIsExporting(false);
@@ -141,6 +141,30 @@ export function useConfigActions() {
141
141
  });
142
142
  },
143
143
  },
144
+ {
145
+ label: "Config > Disable code lens",
146
+ hidden: !(config.display.code_lens ?? true),
147
+ handle: () => {
148
+ handleUserConfig({
149
+ display: {
150
+ ...config.display,
151
+ code_lens: false,
152
+ },
153
+ });
154
+ },
155
+ },
156
+ {
157
+ label: "Config > Enable code lens",
158
+ hidden: config.display.code_lens ?? true,
159
+ handle: () => {
160
+ handleUserConfig({
161
+ display: {
162
+ ...config.display,
163
+ code_lens: true,
164
+ },
165
+ });
166
+ },
167
+ },
144
168
  {
145
169
  label: "Config > Set cell output area: above",
146
170
  hidden: config.display.cell_output === "above",
@@ -51,6 +51,7 @@ import {
51
51
  } from "@/components/ui/dropdown-menu";
52
52
  import { toast } from "@/components/ui/use-toast";
53
53
  import { AiModelId } from "@/core/ai/ids/ids";
54
+ import { AI_SDK_UI_THROTTLE_MS } from "@/core/ai/constants";
54
55
  import { stagedAICellsAtom, useStagedCells } from "@/core/ai/staged-cells";
55
56
  import type { ToolNotebookContext } from "@/core/ai/tools/base";
56
57
  import { useCellActions } from "@/core/cells/cells";
@@ -117,8 +118,7 @@ export const AddCellWithAI: React.FC<{
117
118
  };
118
119
 
119
120
  const { sendMessage, stop, status, addToolOutput } = useChat({
120
- // Throttle the messages and data updates to 100ms
121
- experimental_throttle: 100,
121
+ throttle: AI_SDK_UI_THROTTLE_MS,
122
122
  transport: new StreamingChunkTransport(
123
123
  {
124
124
  api: runtimeManager.getAiURL("completion").toString(),
@@ -27,6 +27,7 @@ import { Label } from "@/components/ui/label";
27
27
  import { Switch } from "@/components/ui/switch";
28
28
  import { Tooltip } from "@/components/ui/tooltip";
29
29
  import { toast } from "@/components/ui/use-toast";
30
+ import { AI_SDK_UI_THROTTLE_MS } from "@/core/ai/constants";
30
31
  import { stripWrappingBackticks } from "@/core/ai/strip-wrapping-backticks";
31
32
  import { type AiCompletionCell, includeOtherCellsAtom } from "@/core/ai/state";
32
33
  import type { CellId } from "@/core/cells/ids";
@@ -124,8 +125,7 @@ export const AiCompletionEditor: React.FC<Props> = ({
124
125
  api: runtimeManager.getAiURL("completion").toString(),
125
126
  headers: runtimeManager.headers(),
126
127
  initialInput: initialPrompt,
127
- // Throttle the messages and data updates to 100ms
128
- experimental_throttle: 100,
128
+ throttle: AI_SDK_UI_THROTTLE_MS,
129
129
  body: {
130
130
  ...(Object.keys(completionBody).length > 0
131
131
  ? completionBody
@@ -0,0 +1,26 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { describe, expect, it } from "vitest";
3
+ import { parseOutline } from "@/core/dom/outline";
4
+ import { findOutlineElements } from "../useActiveOutline";
5
+
6
+ describe("findOutlineElements", () => {
7
+ it.each([
8
+ "Design from the portfolio",
9
+ 'Decorative Art of "Spanish California"',
10
+ `The painter's "Study"`,
11
+ ])("finds an id-less heading named %s", (name) => {
12
+ const html = `<h3>${name}</h3>`;
13
+ document.body.innerHTML = html;
14
+
15
+ const outline = parseOutline({
16
+ mimetype: "text/html",
17
+ timestamp: 0,
18
+ channel: "output",
19
+ data: html,
20
+ });
21
+
22
+ expect(
23
+ findOutlineElements(outline?.items ?? []).map(([element]) => element),
24
+ ).toEqual([document.querySelector("h3")]);
25
+ });
26
+ });
@@ -1,13 +1,61 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { fireEvent, render, screen } from "@testing-library/react";
4
- import { beforeAll, describe, expect, it } from "vitest";
3
+ import { EditorView } from "@codemirror/view";
4
+ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
5
+ import {
6
+ afterAll,
7
+ beforeAll,
8
+ beforeEach,
9
+ describe,
10
+ expect,
11
+ it,
12
+ vi,
13
+ } from "vitest";
5
14
  import { SetupMocks } from "@/__mocks__/common";
6
15
  import { TooltipProvider } from "@/components/ui/tooltip";
7
16
  import { ReadonlyCode } from "../readonly-python-code";
8
17
 
18
+ const themeState = vi.hoisted(() => ({
19
+ value: "light" as "light" | "dark",
20
+ }));
21
+
22
+ let originalRangeGetClientRects: PropertyDescriptor | undefined;
23
+
24
+ vi.mock("@/theme/useTheme", async (importOriginal) => {
25
+ const actual = await importOriginal<typeof import("@/theme/useTheme")>();
26
+ return {
27
+ ...actual,
28
+ useTheme: () => ({ theme: themeState.value }),
29
+ };
30
+ });
31
+
9
32
  beforeAll(() => {
10
33
  SetupMocks.resizeObserver();
34
+ originalRangeGetClientRects = Object.getOwnPropertyDescriptor(
35
+ Range.prototype,
36
+ "getClientRects",
37
+ );
38
+ Object.defineProperty(Range.prototype, "getClientRects", {
39
+ configurable: true,
40
+ value: () => [],
41
+ });
42
+ });
43
+
44
+ afterAll(() => {
45
+ // Keep the shared JSDOM prototype unchanged for other test files.
46
+ if (originalRangeGetClientRects) {
47
+ Object.defineProperty(
48
+ Range.prototype,
49
+ "getClientRects",
50
+ originalRangeGetClientRects,
51
+ );
52
+ } else {
53
+ Reflect.deleteProperty(Range.prototype, "getClientRects");
54
+ }
55
+ });
56
+
57
+ beforeEach(() => {
58
+ themeState.value = "light";
11
59
  });
12
60
 
13
61
  /**
@@ -26,7 +74,71 @@ function isCollapsed(root: ParentNode) {
26
74
  return root.querySelector(".cm")?.classList.contains("opacity-20") ?? false;
27
75
  }
28
76
 
77
+ function getEditorIdentity(view: EditorView) {
78
+ return {
79
+ doc: view.state.doc,
80
+ dom: view.dom,
81
+ contentDOM: view.contentDOM,
82
+ scrollDOM: view.scrollDOM,
83
+ };
84
+ }
85
+
86
+ function expectEditorIdentity(
87
+ view: EditorView,
88
+ identity: ReturnType<typeof getEditorIdentity>,
89
+ ) {
90
+ expect(view.state.doc).toBe(identity.doc);
91
+ expect(view.dom).toBe(identity.dom);
92
+ expect(view.contentDOM).toBe(identity.contentDOM);
93
+ expect(view.scrollDOM).toBe(identity.scrollDOM);
94
+ }
95
+
29
96
  describe("ReadonlyCode", () => {
97
+ it("reconfigures the theme without replacing the editor", async () => {
98
+ let createdView: EditorView | undefined;
99
+ const onCreateEditor = (view: EditorView) => {
100
+ createdView = view;
101
+ };
102
+ const renderCode = (id: string) => (
103
+ <TooltipProvider>
104
+ <ReadonlyCode
105
+ id={id}
106
+ code={"line_1 = 1\nline_2 = 2\nline_3 = 3"}
107
+ showCopyCode={false}
108
+ onCreateEditor={onCreateEditor}
109
+ />
110
+ </TooltipProvider>
111
+ );
112
+
113
+ const { rerender } = render(renderCode("light"));
114
+ await waitFor(() => {
115
+ expect(createdView).toBeDefined();
116
+ });
117
+
118
+ const view = createdView;
119
+ if (!view) {
120
+ throw new Error("ReadonlyCode did not create an editor view");
121
+ }
122
+ const identity = getEditorIdentity(view);
123
+ expect(view.state.facet(EditorView.darkTheme)).toBe(false);
124
+
125
+ themeState.value = "dark";
126
+ rerender(renderCode("dark"));
127
+ await waitFor(() => {
128
+ expect(view.state.facet(EditorView.darkTheme)).toBe(true);
129
+ });
130
+ expect(createdView).toBe(view);
131
+ expectEditorIdentity(view, identity);
132
+
133
+ themeState.value = "light";
134
+ rerender(renderCode("light-again"));
135
+ await waitFor(() => {
136
+ expect(view.state.facet(EditorView.darkTheme)).toBe(false);
137
+ });
138
+ expect(createdView).toBe(view);
139
+ expectEditorIdentity(view, identity);
140
+ });
141
+
30
142
  it("starts collapsed when initiallyHideCode is true", () => {
31
143
  const { container } = renderReadonly({ initiallyHideCode: true });
32
144
  expect(isCollapsed(container)).toBe(true);
@@ -154,6 +154,20 @@ _creds = json.loads(_credentials_json)
154
154
  fs = GoogleDriveFileSystem(creds=_creds, token="service_account", use_listings_cache=False, skip_instance_cache=True)"
155
155
  `;
156
156
 
157
+ exports[`generateStorageCode > Hugging Face > default connection 1`] = `
158
+ "from huggingface_hub import HfApi
159
+
160
+ hf = HfApi()"
161
+ `;
162
+
163
+ exports[`generateStorageCode > Hugging Face > with token from secrets 1`] = `
164
+ "from huggingface_hub import HfApi
165
+ import os
166
+
167
+ _token = os.environ.get("HF_TOKEN")
168
+ hf = HfApi(token=_token)"
169
+ `;
170
+
157
171
  exports[`generateStorageCode > S3 > basic connection with all fields 1`] = `
158
172
  "from obstore.store import S3Store
159
173