@marimo-team/frontend 0.19.7-dev41 → 0.19.7-dev45

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 (78) hide show
  1. package/dist/assets/{CellStatus-DhGipVU-.js → CellStatus-BFim6YKS.js} +1 -1
  2. package/dist/assets/{JsonOutput-B3se7Juu.js → JsonOutput-DkVhF91W.js} +1 -1
  3. package/dist/assets/{MarimoErrorOutput-Cci2wITc.js → MarimoErrorOutput-DWQ55i9r.js} +1 -1
  4. package/dist/assets/{RenderHTML-Co6iQEvR.js → RenderHTML-a-roMXlk.js} +1 -1
  5. package/dist/assets/{add-cell-with-ai-LP5p0BtF.js → add-cell-with-ai-BR7IVCSW.js} +1 -1
  6. package/dist/assets/{add-database-form-_pTLtiHN.js → add-database-form-ClfjsEtC.js} +1 -1
  7. package/dist/assets/{agent-panel-DreQAGPX.js → agent-panel-DSORiKrx.js} +1 -1
  8. package/dist/assets/{ai-model-dropdown-CJGHNqP_.js → ai-model-dropdown-JY5c7nJK.js} +1 -1
  9. package/dist/assets/{app-config-button-DDLHriXt.js → app-config-button-Jz087HD-.js} +1 -1
  10. package/dist/assets/{cell-editor-Bio_oQvd.js → cell-editor-B6FY0jdx.js} +2 -2
  11. package/dist/assets/{cell-link-_-mIiddP.js → cell-link-D2K7clF4.js} +1 -1
  12. package/dist/assets/{cells-BW_4R0Qw.js → cells-Dxpbusw1.js} +37 -37
  13. package/dist/assets/{chat-components-ZGfi_TyH.js → chat-components-B6qBxIzc.js} +1 -1
  14. package/dist/assets/{chat-display-C7-tFQht.js → chat-display-CPulKLCm.js} +1 -1
  15. package/dist/assets/{chat-panel-CfvDUHSP.js → chat-panel-COGD6xs1.js} +1 -1
  16. package/dist/assets/{column-preview-DpVMSlSk.js → column-preview-D3y9pwSu.js} +1 -1
  17. package/dist/assets/{command-B5H3BrRg.js → command-BqcYTnlg.js} +1 -1
  18. package/dist/assets/{command-palette-B0A78_e0.js → command-palette-BaR-SOA3.js} +1 -1
  19. package/dist/assets/{common-DxKcMlJZ.js → common-DuZx4dBP.js} +1 -1
  20. package/dist/assets/{datasource-CpcDqjf_.js → datasource-BA7iQQaN.js} +1 -1
  21. package/dist/assets/{dependency-graph-panel-CLjSJI0K.js → dependency-graph-panel-C27DHbPF.js} +1 -1
  22. package/dist/assets/{documentation-panel-BK_YuaI4.js → documentation-panel-COvuCjB2.js} +1 -1
  23. package/dist/assets/download-DZGL4UAN.js +9 -0
  24. package/dist/assets/{edit-page-CXtTEw1V.js → edit-page-CoNmmXUy.js} +3 -3
  25. package/dist/assets/{error-panel-DulelhA-.js → error-panel-BHPn8JVQ.js} +1 -1
  26. package/dist/assets/{file-explorer-panel-B7hxTwzM.js → file-explorer-panel-CozR65iO.js} +1 -1
  27. package/dist/assets/{floating-outline-Ni_RT38T.js → floating-outline-CY6lTY5s.js} +1 -1
  28. package/dist/assets/{focus-CsiV5LZR.js → focus-BLcChWyg.js} +1 -1
  29. package/dist/assets/{form-B1n-e_X0.js → form-CKnYm6ml.js} +1 -1
  30. package/dist/assets/{globals-ols5LsZP.js → globals-Dd-Dr3p2.js} +1 -1
  31. package/dist/assets/{home-page-MttpX4Nz.js → home-page-Ds6etoNv.js} +1 -1
  32. package/dist/assets/{hooks-DaqNvfLy.js → hooks-B1vW9UPB.js} +1 -1
  33. package/dist/assets/{html-to-image-BVbOFO17.js → html-to-image-DSMiAoU4.js} +1 -1
  34. package/dist/assets/{index-XWeGeEmy.js → index-B_qRlPME.js} +3 -3
  35. package/dist/assets/{kiosk-mode-vMIC4Cbh.js → kiosk-mode-x5QGVyC6.js} +1 -1
  36. package/dist/assets/{layout-CrAbikmI.js → layout-BVZJ0wNL.js} +1 -1
  37. package/dist/assets/{logs-panel-DeNFSwJb.js → logs-panel-W6lnhN9B.js} +1 -1
  38. package/dist/assets/{markdown-renderer-YZb6nuwv.js → markdown-renderer-Bx2DfaSd.js} +1 -1
  39. package/dist/assets/{mode-BCr7l3Th.js → mode-BQL1qb7F.js} +1 -1
  40. package/dist/assets/{name-cell-input-D6uaGwg7.js → name-cell-input-B3FIAxBj.js} +1 -1
  41. package/dist/assets/{outline-panel-DpbnlPhw.js → outline-panel-AJlQUjZ_.js} +1 -1
  42. package/dist/assets/{packages-panel-BkOmsLKm.js → packages-panel-BJsvZ-68.js} +1 -1
  43. package/dist/assets/{panels-Khoe5vWe.js → panels-CZFKs6IJ.js} +1 -1
  44. package/dist/assets/{process-output-TMO5uCHT.js → process-output-DK8a3ADA.js} +1 -1
  45. package/dist/assets/{readonly-python-code-Dv4ZvZ6I.js → readonly-python-code-tAbt2ya8.js} +1 -1
  46. package/dist/assets/{run-page-D7t4ZVs2.js → run-page-BpOFopIQ.js} +1 -1
  47. package/dist/assets/{scratchpad-panel-BzDqHgmy.js → scratchpad-panel-DfCsTT2f.js} +1 -1
  48. package/dist/assets/{session-panel-C9DYV18k.js → session-panel-Dd73AAIv.js} +1 -1
  49. package/dist/assets/{snippets-panel-ypIwat9G.js → snippets-panel-HEdYHO0z.js} +1 -1
  50. package/dist/assets/{state-BrsyJBHJ.js → state-BMdr2nMy.js} +1 -1
  51. package/dist/assets/{switch-Cch0bLxo.js → switch-uE7-SGTN.js} +1 -1
  52. package/dist/assets/{textarea-DSR2T2ft.js → textarea-Dnc9oNCJ.js} +1 -1
  53. package/dist/assets/{tracing-vnJxVAtK.js → tracing-CsZXkSMG.js} +1 -1
  54. package/dist/assets/{tracing-panel-57WGQhyd.js → tracing-panel-DzL1Eutt.js} +2 -2
  55. package/dist/assets/{types-fTSozrNJ.js → types-BjS1POBx.js} +1 -1
  56. package/dist/assets/{useAddCell-CLnCtSpX.js → useAddCell-fK8ZFPPQ.js} +1 -1
  57. package/dist/assets/{useCellActionButton-B1CEV7yf.js → useCellActionButton-BBXD0ZNf.js} +1 -1
  58. package/dist/assets/{useDeleteCell-CR3IczUk.js → useDeleteCell-BFvljUOH.js} +1 -1
  59. package/dist/assets/{useDependencyPanelTab-CJoiLtsW.js → useDependencyPanelTab-4_zo1zmo.js} +1 -1
  60. package/dist/assets/{useNotebookActions-BG12cF9Q.js → useNotebookActions---KuaMmt.js} +1 -1
  61. package/dist/assets/{useRunCells-C50mliNM.js → useRunCells-ChluEx0W.js} +1 -1
  62. package/dist/assets/{useSplitCell-IQsKBoRj.js → useSplitCell-BYSgH1JP.js} +1 -1
  63. package/dist/assets/{utilities.esm-DK107uYJ.js → utilities.esm-CgPvBw6L.js} +1 -1
  64. package/dist/index.html +31 -31
  65. package/package.json +1 -1
  66. package/src/core/ai/tools/__tests__/edit-notebook-tool.test.ts +1 -0
  67. package/src/core/codemirror/cm.ts +1 -0
  68. package/src/core/codemirror/config/extension.ts +3 -0
  69. package/src/core/codemirror/copilot/__tests__/getCodes.test.ts +1 -0
  70. package/src/core/codemirror/facet.ts +4 -1
  71. package/src/core/codemirror/language/__tests__/extension.test.ts +1 -0
  72. package/src/core/codemirror/language/__tests__/utils.test.ts +1 -0
  73. package/src/core/codemirror/language/extension.ts +12 -0
  74. package/src/utils/__tests__/download.test.tsx +89 -45
  75. package/src/utils/__tests__/iframe.test.ts +9 -14
  76. package/src/utils/download.ts +37 -44
  77. package/src/utils/iframe.ts +3 -10
  78. package/dist/assets/download-BN9map2G.js +0 -9
@@ -31,6 +31,7 @@ function createEditor(doc: string) {
31
31
  lspConfig: {},
32
32
  }),
33
33
  cellConfigExtension({
34
+ cellId: "cell1" as CellId,
34
35
  completionConfig: {
35
36
  activate_on_typing: true,
36
37
  signature_hint_on_typing: false,
@@ -15,6 +15,7 @@ import type {
15
15
  LSPConfig,
16
16
  } from "@/core/config/config-schema";
17
17
  import type { HotkeyProvider } from "@/core/hotkeys/hotkeys";
18
+ import { Logger } from "@/utils/Logger";
18
19
  import { clamp } from "@/utils/math";
19
20
  import {
20
21
  cellIdState,
@@ -310,6 +311,17 @@ export function reconfigureLanguageEffect(
310
311
  const language = view.state.field(languageAdapterState);
311
312
  const placeholderType = view.state.facet(placeholderState);
312
313
  const cellId = view.state.facet(cellIdState);
314
+
315
+ if (cellId === undefined) {
316
+ Logger.error("Cell ID is undefined in reconfigureLanguageEffect");
317
+ }
318
+ if (placeholderType === undefined) {
319
+ Logger.error("Placeholder type is undefined in reconfigureLanguageEffect");
320
+ }
321
+ if (completionConfig === undefined) {
322
+ Logger.error("Completion config is undefined in reconfigureLanguageEffect");
323
+ }
324
+
313
325
  return languageCompartment.reconfigure(
314
326
  language.getExtension(
315
327
  cellId,
@@ -166,13 +166,63 @@ describe("getImageDataUrlForCell", () => {
166
166
  );
167
167
  });
168
168
 
169
- it("should restore original overflow style after capture", async () => {
169
+ it("should pass style options to prevent clipping", async () => {
170
+ vi.mocked(toPng).mockResolvedValue(mockDataUrl);
171
+
172
+ await getImageDataUrlForCell("cell-1" as CellId);
173
+
174
+ expect(toPng).toHaveBeenCalledWith(
175
+ mockElement,
176
+ expect.objectContaining({
177
+ style: {
178
+ maxHeight: "none",
179
+ overflow: "visible",
180
+ },
181
+ }),
182
+ );
183
+ });
184
+
185
+ it("should pass scrollHeight as height option", async () => {
186
+ // Set up element with scrollHeight
187
+ Object.defineProperty(mockElement, "scrollHeight", {
188
+ value: 500,
189
+ configurable: true,
190
+ });
191
+ vi.mocked(toPng).mockResolvedValue(mockDataUrl);
192
+
193
+ await getImageDataUrlForCell("cell-1" as CellId);
194
+
195
+ expect(toPng).toHaveBeenCalledWith(
196
+ mockElement,
197
+ expect.objectContaining({
198
+ height: 500,
199
+ }),
200
+ );
201
+ });
202
+
203
+ it("should pass scrollbar hiding styles via extraStyleContent", async () => {
204
+ vi.mocked(toPng).mockResolvedValue(mockDataUrl);
205
+
206
+ await getImageDataUrlForCell("cell-1" as CellId);
207
+
208
+ expect(toPng).toHaveBeenCalledWith(
209
+ mockElement,
210
+ expect.objectContaining({
211
+ extraStyleContent: expect.stringContaining("scrollbar-width: none"),
212
+ }),
213
+ );
214
+ });
215
+
216
+ it("should not modify the live DOM element", async () => {
170
217
  mockElement.style.overflow = "hidden";
218
+ mockElement.style.maxHeight = "100px";
171
219
  vi.mocked(toPng).mockResolvedValue(mockDataUrl);
172
220
 
173
221
  await getImageDataUrlForCell("cell-1" as CellId);
174
222
 
223
+ // DOM should remain unchanged
175
224
  expect(mockElement.style.overflow).toBe("hidden");
225
+ expect(mockElement.style.maxHeight).toBe("100px");
176
226
  });
177
227
 
178
228
  it("should throw error on failure", async () => {
@@ -183,34 +233,20 @@ describe("getImageDataUrlForCell", () => {
183
233
  );
184
234
  });
185
235
 
186
- it("should cleanup even on failure", async () => {
187
- mockElement.style.overflow = "scroll";
188
- vi.mocked(toPng).mockRejectedValue(new Error("Capture failed"));
189
-
190
- await expect(getImageDataUrlForCell("cell-1" as CellId)).rejects.toThrow();
191
-
192
- expect(document.body.classList.contains("printing")).toBe(false);
193
- expect(mockElement.style.overflow).toBe("scroll");
194
- });
195
-
196
236
  it("should handle concurrent captures correctly", async () => {
197
237
  // Create a second element
198
238
  const mockElement2 = document.createElement("div");
199
239
  mockElement2.id = CellOutputId.create("cell-2" as CellId);
200
240
  document.body.append(mockElement2);
201
241
 
202
- vi.mocked(toPng).mockImplementation(async () => {
203
- // body.printing should not be added during cell captures
204
- expect(document.body.classList.contains("printing")).toBe(false);
205
- return mockDataUrl;
206
- });
242
+ vi.mocked(toPng).mockResolvedValue(mockDataUrl);
207
243
 
208
244
  const capture1 = getImageDataUrlForCell("cell-1" as CellId);
209
245
  const capture2 = getImageDataUrlForCell("cell-2" as CellId);
210
246
 
211
247
  await Promise.all([capture1, capture2]);
212
248
 
213
- expect(document.body.classList.contains("printing")).toBe(false);
249
+ expect(toPng).toHaveBeenCalledTimes(2);
214
250
 
215
251
  mockElement2.remove();
216
252
  });
@@ -266,23 +302,6 @@ describe("downloadHTMLAsImage", () => {
266
302
  expect(mockAnchor.click).toHaveBeenCalled();
267
303
  });
268
304
 
269
- it("should add body.printing class without prepare function", async () => {
270
- vi.mocked(toPng).mockImplementation(async () => {
271
- expect(document.body.classList.contains("printing")).toBe(true);
272
- return mockDataUrl;
273
- });
274
-
275
- await downloadHTMLAsImage({ element: mockElement, filename: "test" });
276
- });
277
-
278
- it("should remove body.printing class after download without prepare", async () => {
279
- vi.mocked(toPng).mockResolvedValue(mockDataUrl);
280
-
281
- await downloadHTMLAsImage({ element: mockElement, filename: "test" });
282
-
283
- expect(document.body.classList.contains("printing")).toBe(false);
284
- });
285
-
286
305
  it("should use prepare function when provided", async () => {
287
306
  vi.mocked(toPng).mockResolvedValue(mockDataUrl);
288
307
  const cleanup = vi.fn();
@@ -382,13 +401,30 @@ describe("downloadCellOutputAsImage", () => {
382
401
  vi.restoreAllMocks();
383
402
  });
384
403
 
385
- it("should return early if element not found", async () => {
404
+ it("should show error toast if element not found", async () => {
386
405
  await downloadCellOutputAsImage("nonexistent" as CellId, "test");
387
406
 
388
407
  expect(toPng).not.toHaveBeenCalled();
389
408
  expect(Logger.error).toHaveBeenCalledWith(
390
409
  "Output element not found for cell nonexistent",
391
410
  );
411
+ expect(toast).toHaveBeenCalledWith({
412
+ title: "Failed to download PNG",
413
+ description: expect.any(String),
414
+ variant: "danger",
415
+ });
416
+ });
417
+
418
+ it("should show error toast if toPng fails", async () => {
419
+ vi.mocked(toPng).mockRejectedValue(new Error("Screenshot failed"));
420
+
421
+ await downloadCellOutputAsImage("cell-1" as CellId, "result");
422
+
423
+ expect(toast).toHaveBeenCalledWith({
424
+ title: "Failed to download PNG",
425
+ description: expect.stringContaining("Screenshot failed"),
426
+ variant: "danger",
427
+ });
392
428
  });
393
429
 
394
430
  it("should download cell output as image", async () => {
@@ -406,24 +442,32 @@ describe("downloadCellOutputAsImage", () => {
406
442
  expect(mockAnchor.download).toBe("result.png");
407
443
  });
408
444
 
409
- it("should apply cell-specific preparation", async () => {
410
- vi.mocked(toPng).mockImplementation(async () => {
411
- // Check that cell-specific classes are applied
412
- expect(mockElement.style.overflow).toBe("visible");
413
- return mockDataUrl;
414
- });
445
+ it("should pass style options to toPng for full content capture", async () => {
446
+ vi.mocked(toPng).mockResolvedValue(mockDataUrl);
415
447
 
416
448
  await downloadCellOutputAsImage("cell-1" as CellId, "result");
449
+
450
+ expect(toPng).toHaveBeenCalledWith(
451
+ mockElement,
452
+ expect.objectContaining({
453
+ style: {
454
+ maxHeight: "none",
455
+ overflow: "visible",
456
+ },
457
+ }),
458
+ );
417
459
  });
418
460
 
419
- it("should cleanup after download", async () => {
420
- mockElement.style.overflow = "visible";
461
+ it("should not modify the live DOM element", async () => {
462
+ mockElement.style.overflow = "hidden";
463
+ mockElement.style.maxHeight = "100px";
421
464
  vi.mocked(toPng).mockResolvedValue(mockDataUrl);
422
465
 
423
466
  await downloadCellOutputAsImage("cell-1" as CellId, "result");
424
467
 
425
- expect(document.body.classList.contains("printing")).toBe(false);
426
- expect(mockElement.style.overflow).toBe("visible");
468
+ // DOM should remain unchanged
469
+ expect(mockElement.style.overflow).toBe("hidden");
470
+ expect(mockElement.style.maxHeight).toBe("100px");
427
471
  });
428
472
  });
429
473
 
@@ -1,13 +1,8 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
- import { captureIframeAsImage } from "../iframe";
3
+ import { captureExternalIframes } from "../iframe";
4
4
 
5
- // Mock html-to-image
6
- vi.mock("html-to-image", () => ({
7
- toPng: vi.fn().mockResolvedValue("data:image/png;base64,mock"),
8
- }));
9
-
10
- describe("captureIframeAsImage", () => {
5
+ describe("captureExternalIframes", () => {
11
6
  const originalCreateElement = document.createElement.bind(document);
12
7
 
13
8
  beforeEach(() => {
@@ -53,7 +48,7 @@ describe("captureIframeAsImage", () => {
53
48
  const element = document.createElement("div");
54
49
  element.innerHTML = "<p>No iframe here</p>";
55
50
 
56
- const result = await captureIframeAsImage(element);
51
+ const result = await captureExternalIframes(element);
57
52
  expect(result).toBeNull();
58
53
  });
59
54
 
@@ -61,7 +56,7 @@ describe("captureIframeAsImage", () => {
61
56
  const element = document.createElement("div");
62
57
  element.innerHTML = '<iframe src="https://external.com/page"></iframe>';
63
58
 
64
- const result = await captureIframeAsImage(element);
59
+ const result = await captureExternalIframes(element);
65
60
 
66
61
  expect(result).not.toBeNull();
67
62
  expect(result).toMatch(/^data:image\/png;base64,/);
@@ -72,7 +67,7 @@ describe("captureIframeAsImage", () => {
72
67
  element.innerHTML =
73
68
  '<iframe src="https://www.openstreetmap.org/export/embed.html"></iframe>';
74
69
 
75
- const result = await captureIframeAsImage(element);
70
+ const result = await captureExternalIframes(element);
76
71
 
77
72
  expect(result).not.toBeNull();
78
73
  expect(result).toMatch(/^data:image\/png;base64,/);
@@ -85,7 +80,7 @@ describe("captureIframeAsImage", () => {
85
80
  element.append(iframe);
86
81
 
87
82
  // The iframe has no body accessible in jsdom
88
- const result = await captureIframeAsImage(element);
83
+ const result = await captureExternalIframes(element);
89
84
 
90
85
  // In jsdom, contentDocument may not be accessible
91
86
  expect(result).toBeNull();
@@ -96,7 +91,7 @@ describe("captureIframeAsImage", () => {
96
91
  element.innerHTML = '<iframe src="./@file/123.html"></iframe>';
97
92
 
98
93
  // Same-origin relative URL, but contentDocument not accessible in jsdom
99
- const result = await captureIframeAsImage(element);
94
+ const result = await captureExternalIframes(element);
100
95
 
101
96
  // Returns null because jsdom can't access contentDocument for file URLs
102
97
  expect(result).toBeNull();
@@ -113,7 +108,7 @@ describe("captureIframeAsImage", () => {
113
108
  const element = document.createElement("div");
114
109
  element.innerHTML = `<iframe src="${url}"></iframe>`;
115
110
 
116
- const result = await captureIframeAsImage(element);
111
+ const result = await captureExternalIframes(element);
117
112
 
118
113
  expect(result).not.toBeNull();
119
114
  expect(result).toMatch(/^data:image\/png;base64,/);
@@ -128,7 +123,7 @@ describe("captureIframeAsImage", () => {
128
123
  const element = document.createElement("div");
129
124
  element.innerHTML = `<iframe src="${url}"></iframe>`;
130
125
 
131
- const result = await captureIframeAsImage(element);
126
+ const result = await captureExternalIframes(element);
132
127
 
133
128
  // In jsdom, these return null because contentDocument isn't accessible
134
129
  // but they should NOT return a placeholder (which would indicate external detection)
@@ -8,7 +8,7 @@ import { Filenames } from "@/utils/filenames";
8
8
  import { Paths } from "@/utils/paths";
9
9
  import { prettyError } from "./errors";
10
10
  import { toPng } from "./html-to-image";
11
- import { captureIframeAsImage } from "./iframe";
11
+ import { captureExternalIframes } from "./iframe";
12
12
  import { Logger } from "./Logger";
13
13
  import { ProgressState } from "./progress";
14
14
  import { ToastProgress } from "./toast-progress";
@@ -46,24 +46,6 @@ function findElementForCell(cellId: CellId): HTMLElement | undefined {
46
46
  return element;
47
47
  }
48
48
 
49
- /**
50
- * Prepare a cell element for screenshot capture.
51
- *
52
- * @param element - The cell output element to prepare
53
- * @returns A cleanup function to restore the element's original state
54
- */
55
- function prepareCellElementForScreenshot(element: HTMLElement) {
56
- const originalOverflow = element.style.overflow;
57
- const maxHeight = element.style.maxHeight;
58
- element.style.overflow = "visible";
59
- element.style.maxHeight = "none";
60
-
61
- return () => {
62
- element.style.overflow = originalOverflow;
63
- element.style.maxHeight = maxHeight;
64
- };
65
- }
66
-
67
49
  const THRESHOLD_TIME_MS = 500;
68
50
  const HIDE_SCROLLBAR_STYLES = `
69
51
  * { scrollbar-width: none; -ms-overflow-style: none; }
@@ -84,30 +66,33 @@ export async function getImageDataUrlForCell(
84
66
  return;
85
67
  }
86
68
 
87
- const iframeDataUrl = await captureIframeAsImage(element);
88
- if (iframeDataUrl) {
89
- return iframeDataUrl;
69
+ // TODO: This doesn't handle external iframes + normal elements together (eg. in vstack).
70
+ // It will return the iframe only
71
+ const externalIframeDataUrl = await captureExternalIframes(element);
72
+ if (externalIframeDataUrl) {
73
+ return externalIframeDataUrl;
90
74
  }
91
75
 
92
- const cleanup = prepareCellElementForScreenshot(element);
93
-
94
- try {
95
- const startTime = Date.now();
96
- const dataUrl = await toPng(element, {
97
- extraStyleContent: HIDE_SCROLLBAR_STYLES,
98
- });
99
- const timeTaken = Date.now() - startTime;
100
- if (timeTaken > THRESHOLD_TIME_MS) {
101
- Logger.debug(
102
- "toPng operation for element",
103
- element,
104
- `took ${timeTaken} ms (exceeds threshold)`,
105
- );
106
- }
107
- return dataUrl;
108
- } finally {
109
- cleanup();
76
+ const startTime = Date.now();
77
+ const dataUrl = await toPng(element, {
78
+ extraStyleContent: HIDE_SCROLLBAR_STYLES,
79
+ // Add these styles so the element output is not clipped
80
+ // Width can be clipped since pdf has limited width
81
+ style: {
82
+ maxHeight: "none",
83
+ overflow: "visible",
84
+ },
85
+ height: element.scrollHeight,
86
+ });
87
+ const timeTaken = Date.now() - startTime;
88
+ if (timeTaken > THRESHOLD_TIME_MS) {
89
+ Logger.debug(
90
+ "toPng operation for element",
91
+ element,
92
+ `took ${timeTaken} ms (exceeds threshold)`,
93
+ );
110
94
  }
95
+ return dataUrl;
111
96
  }
112
97
 
113
98
  /**
@@ -117,11 +102,19 @@ export async function downloadCellOutputAsImage(
117
102
  cellId: CellId,
118
103
  filename: string,
119
104
  ) {
120
- const dataUrl = await getImageDataUrlForCell(cellId);
121
- if (!dataUrl) {
122
- return;
105
+ try {
106
+ const dataUrl = await getImageDataUrlForCell(cellId);
107
+ if (!dataUrl) {
108
+ throw new Error("Failed to get image data URL");
109
+ }
110
+ downloadByURL(dataUrl, Filenames.toPNG(filename));
111
+ } catch (error) {
112
+ toast({
113
+ title: "Failed to download PNG",
114
+ description: prettyError(error),
115
+ variant: "danger",
116
+ });
123
117
  }
124
- downloadByURL(dataUrl, Filenames.toPNG(filename));
125
118
  }
126
119
 
127
120
  export const ADD_PRINTING_CLASS = (): (() => void) => {
@@ -1,7 +1,5 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { toPng } from "./html-to-image";
4
-
5
3
  const PLACEHOLDER_WIDTH = 320;
6
4
  const PLACEHOLDER_HEIGHT = 180;
7
5
 
@@ -95,11 +93,11 @@ function createPlaceholderImage(url: string | null): string {
95
93
  }
96
94
 
97
95
  /**
98
- * Capture an iframe as a PNG image. We need to do this because external iframes are not supported by html-to-image.
96
+ * Capture external iframes as a PNG image. External iframes are not supported by html-to-image.
99
97
  * @param element - The element to capture the iframe from
100
98
  * @returns The image data URL of the iframe, or a placeholder image if the iframe is external
101
99
  */
102
- export async function captureIframeAsImage(
100
+ export async function captureExternalIframes(
103
101
  element: HTMLElement,
104
102
  ): Promise<string | null> {
105
103
  const iframe = element.querySelector("iframe");
@@ -133,10 +131,5 @@ export async function captureIframeAsImage(
133
131
  }
134
132
  }
135
133
 
136
- // Capture the iframe content
137
- try {
138
- return await toPng(doc.body);
139
- } catch {
140
- return createPlaceholderImage(null);
141
- }
134
+ return null;
142
135
  }
@@ -1,9 +0,0 @@
1
- var ie=Object.defineProperty;var le=(t,e,r)=>e in t?ie(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r;var D=(t,e,r)=>le(t,typeof e!="symbol"?e+"":e,r);import{s as I,t as V}from"./chunk-LvLJmgfZ.js";import{t as G}from"./react-BGmjiNul.js";import{ii as se,zt as ue}from"./cells-BW_4R0Qw.js";import{t as H}from"./compiler-runtime-DeeZ7FnK.js";import{d as q}from"./hotkeys-BHHWjLlp.js";import{t as ce}from"./jsx-runtime-ZmTK25f3.js";import{t as B}from"./cn-BKtXLv3a.js";import{t as de}from"./requests-BsVD4CdD.js";import{t as fe}from"./createLucideIcon-CnW3RofX.js";import{t as k}from"./use-toast-rmUWldD_.js";import{d as me}from"./popover-Gz-GJzym.js";import{r as pe}from"./errors-2SszdW9t.js";import{t as W}from"./dist-CdxIjAOP.js";import{t as O}from"./html-to-image-BVbOFO17.js";var ve=fe("chevron-left",[["path",{d:"m15 18-6-6 6-6",key:"1wnfg3"}]]),z=t=>{let e,r=new Set,a=(l,s)=>{let c=typeof l=="function"?l(e):l;if(!Object.is(c,e)){let d=e;e=s??(typeof c!="object"||!c)?c:Object.assign({},e,c),r.forEach(u=>u(e,d))}},n=()=>e,i={setState:a,getState:n,getInitialState:()=>o,subscribe:l=>(r.add(l),()=>r.delete(l)),destroy:()=>{r.clear()}},o=e=t(a,n,i);return i},he=t=>t?z(t):z,ge=V((t=>{var e=G(),r=me();function a(d,u){return d===u&&(d!==0||1/d==1/u)||d!==d&&u!==u}var n=typeof Object.is=="function"?Object.is:a,i=r.useSyncExternalStore,o=e.useRef,l=e.useEffect,s=e.useMemo,c=e.useDebugValue;t.useSyncExternalStoreWithSelector=function(d,u,f,g,m){var p=o(null);if(p.current===null){var w={hasValue:!1,value:null};p.current=w}else w=p.current;p=s(function(){function A(y){if(!U){if(U=!0,S=y,y=g(y),m!==void 0&&w.hasValue){var N=w.value;if(m(N,y))return $=N}return $=y}if(N=$,n(S,y))return N;var F=g(y);return m!==void 0&&m(N,F)?(S=y,N):(S=y,$=F)}var U=!1,S,$,C=f===void 0?null:f;return[function(){return A(u())},C===null?void 0:function(){return A(C())}]},[u,f,g,m]);var b=i(d,p[0],p[1]);return l(function(){w.hasValue=!0,w.value=b},[b]),c(b),b}})),ye=V(((t,e)=>{e.exports=ge()}));const h={toMarkdown:t=>h.replace(t,"md"),toHTML:t=>h.replace(t,"html"),toPNG:t=>h.replace(t,"png"),toPDF:t=>h.replace(t,"pdf"),toPY:t=>h.replace(t,"py"),withoutExtension:t=>{let e=t.split(".");return e.length===1?t:e.slice(0,-1).join(".")},replace:(t,e)=>t.endsWith(`.${e}`)?t:`${h.withoutExtension(t)}.${e}`};var P=320,j=180;function X(t){if(!t||t==="about:blank")return null;try{let e=new URL(t,window.location.href);return e.origin===window.location.origin?null:e.href}catch{return t}}function xe(t,e,r){let a=[],n="";for(let i of e){let o=n+i;t.measureText(o).width<=r?n=o:(n&&a.push(n),n=i)}return n&&a.push(n),a}function L(t){let e=window.devicePixelRatio||1,r=document.createElement("canvas");r.width=P*e,r.height=j*e;let a=r.getContext("2d");if(!a)return r.toDataURL("image/png");a.scale(e,e),a.fillStyle="#f3f4f6",a.fillRect(0,0,P,j),a.strokeStyle="#d1d5db",a.strokeRect(.5,.5,P-1,j-1),a.fillStyle="#6b7280",a.font="8px sans-serif",a.textAlign="center",a.textBaseline="middle";let n=P-32,i=t?xe(a,t,n):[],o=(j-(1+i.length)*14)/2+14/2;a.fillText("External iframe",P/2,o),o+=14;for(let l of i)a.fillText(l,P/2,o),o+=14;return r.toDataURL("image/png")}async function we(t){var n;let e=t.querySelector("iframe");if(!e)return null;let r=X(e.getAttribute("src"));if(r)return L(r);let a;try{let i=e.contentDocument||((n=e.contentWindow)==null?void 0:n.document);if(!(i!=null&&i.body))return null;a=i}catch{return L(null)}for(let i of a.querySelectorAll("iframe")){let o=X(i.getAttribute("src"));if(o)return L(o)}try{return await O(a.body)}catch{return L(null)}}var Y=class oe{constructor(e){D(this,"progress",0);D(this,"listeners",new Set);this.total=e}static indeterminate(){return new oe("indeterminate")}addTotal(e){this.total==="indeterminate"?this.total=e:this.total+=e,this.notifyListeners()}increment(e){this.progress+=e,this.notifyListeners()}getProgress(){return this.total==="indeterminate"?"indeterminate":this.progress/this.total*100}subscribe(e){return this.listeners.add(e),()=>{this.listeners.delete(e)}}notifyListeners(){let e=this.getProgress();for(let r of this.listeners)r(e)}},v=I(G(),1),x=I(ce(),1);function be(t,e=[]){let r=[];function a(i,o){let l=v.createContext(o);l.displayName=i+"Context";let s=r.length;r=[...r,o];let c=u=>{var b;let{scope:f,children:g,...m}=u,p=((b=f==null?void 0:f[t])==null?void 0:b[s])||l,w=v.useMemo(()=>m,Object.values(m));return(0,x.jsx)(p.Provider,{value:w,children:g})};c.displayName=i+"Provider";function d(u,f){var p;let g=((p=f==null?void 0:f[t])==null?void 0:p[s])||l,m=v.useContext(g);if(m)return m;if(o!==void 0)return o;throw Error(`\`${u}\` must be used within \`${i}\``)}return[c,d]}let n=()=>{let i=r.map(o=>v.createContext(o));return function(o){let l=(o==null?void 0:o[t])||i;return v.useMemo(()=>({[`__scope${t}`]:{...o,[t]:l}}),[o,l])}};return n.scopeName=t,[a,Ne(n,...e)]}function Ne(...t){let e=t[0];if(t.length===1)return e;let r=()=>{let a=t.map(n=>({useScope:n(),scopeName:n.scopeName}));return function(n){let i=a.reduce((o,{useScope:l,scopeName:s})=>{let c=l(n)[`__scope${s}`];return{...o,...c}},{});return v.useMemo(()=>({[`__scope${e.scopeName}`]:i}),[i])}};return r.scopeName=e.scopeName,r}var _="Progress",M=100,[Pe,nt]=be(_),[Se,$e]=Pe(_),J=v.forwardRef((t,e)=>{let{__scopeProgress:r,value:a=null,max:n,getValueLabel:i=je,...o}=t;(n||n===0)&&!ee(n)&&console.error(Le(`${n}`,"Progress"));let l=ee(n)?n:M;a!==null&&!te(a,l)&&console.error(Ee(`${a}`,"Progress"));let s=te(a,l)?a:null,c=E(s)?i(s,l):void 0;return(0,x.jsx)(Se,{scope:r,value:s,max:l,children:(0,x.jsx)(W.div,{"aria-valuemax":l,"aria-valuemin":0,"aria-valuenow":E(s)?s:void 0,"aria-valuetext":c,role:"progressbar","data-state":Z(s,l),"data-value":s??void 0,"data-max":l,...o,ref:e})})});J.displayName=_;var K="ProgressIndicator",Q=v.forwardRef((t,e)=>{let{__scopeProgress:r,...a}=t,n=$e(K,r);return(0,x.jsx)(W.div,{"data-state":Z(n.value,n.max),"data-value":n.value??void 0,"data-max":n.max,...a,ref:e})});Q.displayName=K;function je(t,e){return`${Math.round(t/e*100)}%`}function Z(t,e){return t==null?"indeterminate":t===e?"complete":"loading"}function E(t){return typeof t=="number"}function ee(t){return E(t)&&!isNaN(t)&&t>0}function te(t,e){return E(t)&&!isNaN(t)&&t<=e&&t>=0}function Le(t,e){return`Invalid prop \`max\` of value \`${t}\` supplied to \`${e}\`. Only numbers greater than 0 are valid max values. Defaulting to \`${M}\`.`}function Ee(t,e){return`Invalid prop \`value\` of value \`${t}\` supplied to \`${e}\`. The \`value\` prop must be:
2
- - a positive number
3
- - less than the value passed to \`max\` (or ${M} if no \`max\` prop is set)
4
- - \`null\` or \`undefined\` if the progress is indeterminate.
5
-
6
- Defaulting to \`null\`.`}var re=J,Re=Q,De=H(),T=v.forwardRef((t,e)=>{let r=(0,De.c)(20),a,n,i,o;r[0]===t?(a=r[1],n=r[2],i=r[3],o=r[4]):({className:a,value:o,indeterminate:n,...i}=t,r[0]=t,r[1]=a,r[2]=n,r[3]=i,r[4]=o);let l;r[5]===a?l=r[6]:(l=B("relative h-2 w-full overflow-hidden rounded-full bg-primary/20",a),r[5]=a,r[6]=l);let s=n?"w-1/3 animate-progress-indeterminate":"w-full transition-transform duration-300 ease-out",c;r[7]===s?c=r[8]:(c=B("h-full flex-1 bg-primary",s),r[7]=s,r[8]=c);let d;r[9]!==n||r[10]!==o?(d=n?void 0:{transform:`translateX(-${100-(o||0)}%)`},r[9]=n,r[10]=o,r[11]=d):d=r[11];let u;r[12]!==c||r[13]!==d?(u=(0,x.jsx)(Re,{className:c,style:d}),r[12]=c,r[13]=d,r[14]=u):u=r[14];let f;return r[15]!==i||r[16]!==e||r[17]!==l||r[18]!==u?(f=(0,x.jsx)(re,{ref:e,className:l,...i,children:u}),r[15]=i,r[16]=e,r[17]=l,r[18]=u,r[19]=f):f=r[19],f});T.displayName=re.displayName;var ke=H();const Oe=t=>{let e=(0,ke.c)(13),{progress:r,showPercentage:a}=t,n=a===void 0?!1:a,i,o;e[0]===r?(i=e[1],o=e[2]):(i=g=>r.subscribe(g),o=()=>r.getProgress(),e[0]=r,e[1]=i,e[2]=o);let l=(0,v.useSyncExternalStore)(i,o),s=l==="indeterminate"||l===100,c=s?void 0:l,d;e[3]!==s||e[4]!==c?(d=(0,x.jsx)(T,{value:c,indeterminate:s}),e[3]=s,e[4]=c,e[5]=d):d=e[5];let u;e[6]!==s||e[7]!==n||e[8]!==l?(u=!s&&n&&(0,x.jsxs)("div",{className:"mt-1 text-xs text-muted-foreground text-right",children:[Math.round(l),"%"]}),e[6]=s,e[7]=n,e[8]=l,e[9]=u):u=e[9];let f;return e[10]!==d||e[11]!==u?(f=(0,x.jsxs)("div",{className:"mt-2 w-full min-w-[200px]",children:[d,u]}),e[10]=d,e[11]=u,e[12]=f):f=e[12],f};async function _e(t,e){let r=Y.indeterminate(),a=k({title:t,description:v.createElement(Oe,{progress:r}),duration:1/0});try{let n=await e(r);return a.dismiss(),n}catch(n){throw a.dismiss(),n}}function Me(t){let e=document.getElementById(se.create(t));if(!e){q.error(`Output element not found for cell ${t}`);return}return e}function Te(t){let e=t.style.overflow,r=t.style.maxHeight;return t.style.overflow="visible",t.style.maxHeight="none",()=>{t.style.overflow=e,t.style.maxHeight=r}}var Ae=500,Ue=`
7
- * { scrollbar-width: none; -ms-overflow-style: none; }
8
- *::-webkit-scrollbar { display: none; }
9
- `;async function ne(t){let e=Me(t);if(!e)return;let r=await we(e);if(r)return r;let a=Te(e);try{let n=Date.now(),i=await O(e,{extraStyleContent:Ue}),o=Date.now()-n;return o>Ae&&q.debug("toPng operation for element",e,`took ${o} ms (exceeds threshold)`),i}finally{a()}}async function Ce(t,e){let r=await ne(t);r&&R(r,h.toPNG(e))}const Fe=()=>(document.body.classList.add("printing"),()=>{document.body.classList.remove("printing")});async function Ie(t){let{element:e,filename:r,prepare:a}=t,n=document.getElementById("App"),i=(n==null?void 0:n.scrollTop)??0,o;a&&(o=a(e));try{R(await O(e),h.toPNG(r))}catch{k({title:"Error",description:"Failed to download as PNG.",variant:"danger"})}finally{o==null||o(),document.body.classList.contains("printing")&&document.body.classList.remove("printing"),requestAnimationFrame(()=>{n==null||n.scrollTo(0,i)})}}function R(t,e){let r=document.createElement("a");r.href=t,r.download=e,r.click(),r.remove()}function ae(t,e){let r=URL.createObjectURL(t);R(r,e),URL.revokeObjectURL(r)}async function Ve(t){let e=de(),{filename:r,webpdf:a}=t;try{let n=await e.exportAsPDF({webpdf:a}),i=ue.basename(r);ae(n,h.toPDF(i))}catch(n){throw k({title:"Failed to download",description:pe(n),variant:"danger"}),n}}export{Ce as a,_e as c,h as d,ye as f,R as i,T as l,ve as m,Ve as n,Ie as o,he as p,ae as r,ne as s,Fe as t,Y as u};