@marimo-team/frontend 0.19.7-dev35 → 0.19.7-dev37

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 (84) hide show
  1. package/dist/assets/{JsonOutput-BGSqWZkD.js → JsonOutput-DtidtKaJ.js} +2 -2
  2. package/dist/assets/{MarimoErrorOutput-DFV7O8tN.js → MarimoErrorOutput-Cci2wITc.js} +1 -1
  3. package/dist/assets/{add-cell-with-ai-gDjJxct8.js → add-cell-with-ai-DT1ae2MK.js} +1 -1
  4. package/dist/assets/{add-database-form-CqEjMrq2.js → add-database-form-_pTLtiHN.js} +1 -1
  5. package/dist/assets/{agent-panel-D_hcgMlT.js → agent-panel-oLGqa4bG.js} +1 -1
  6. package/dist/assets/{ai-model-dropdown-C_l_zAXd.js → ai-model-dropdown-BL-PaF8o.js} +1 -1
  7. package/dist/assets/{app-config-button-CM05VD_Y.js → app-config-button-BiwYEPE8.js} +1 -1
  8. package/dist/assets/{cell-editor-ihhw9Ql3.js → cell-editor-B3U1SnYJ.js} +1 -1
  9. package/dist/assets/{chat-display-yGAtJXm0.js → chat-display-B34MaCGM.js} +1 -1
  10. package/dist/assets/{chat-panel-D3T1CjpZ.js → chat-panel-fuQFRvFm.js} +1 -1
  11. package/dist/assets/{column-preview-DbqM1diq.js → column-preview-B-dViv1i.js} +1 -1
  12. package/dist/assets/{command-palette-C2L80RlN.js → command-palette-KuNgJNix.js} +1 -1
  13. package/dist/assets/{common-D32S2RVD.js → common-DxKcMlJZ.js} +1 -1
  14. package/dist/assets/{dependency-graph-panel-C7ZXgECY.js → dependency-graph-panel-BZEIOxVz.js} +1 -1
  15. package/dist/assets/download-Bwa9P-Pz.js +6 -0
  16. package/dist/assets/{dropdown-menu-df9T83C0.js → dropdown-menu-B-6unW-7.js} +1 -1
  17. package/dist/assets/edit-page-DBFIML4p.js +13 -0
  18. package/dist/assets/{error-panel-DKIWwbhe.js → error-panel-DulelhA-.js} +1 -1
  19. package/dist/assets/{file-explorer-panel-D4VHkjMW.js → file-explorer-panel-Dn9tKw3E.js} +1 -1
  20. package/dist/assets/{form-XjMNGyzu.js → form-B1n-e_X0.js} +1 -1
  21. package/dist/assets/{glide-data-editor-BBSxoBI-.js → glide-data-editor-HGkaxqOo.js} +1 -1
  22. package/dist/assets/home-page--XVUAUCM.js +4 -0
  23. package/dist/assets/hooks-B1nUQK2T.js +1 -0
  24. package/dist/assets/{html-to-image-DKvXQkl5.js → html-to-image-Cu1p0tCK.js} +2 -2
  25. package/dist/assets/{index-DmMvDRRC.css → index-Bj5F80Z9.css} +1 -1
  26. package/dist/assets/{index-BRCNo5ma.js → index-DTpRqi46.js} +5 -5
  27. package/dist/assets/{layout-DiDoTAUA.js → layout-KY92f2Sm.js} +3 -3
  28. package/dist/assets/{markdown-renderer-xQJ1KM4c.js → markdown-renderer-Dpn5NCvn.js} +1 -1
  29. package/dist/assets/{packages-panel-D53RuG3X.js → packages-panel-CBc59eNR.js} +1 -1
  30. package/dist/assets/{panels-BhznEx5N.js → panels-B0B71dYl.js} +1 -1
  31. package/dist/assets/{popover-D16ZremR.js → popover-Gz-GJzym.js} +1 -1
  32. package/dist/assets/{readonly-python-code-kImQwJ5f.js → readonly-python-code-CCwpyiLX.js} +1 -1
  33. package/dist/assets/{renderShortcut-DHc-p-_c.js → renderShortcut-DEwfrKeS.js} +1 -1
  34. package/dist/assets/run-page-Dug0EU2T.js +1 -0
  35. package/dist/assets/{scratchpad-panel-C8wx6cRl.js → scratchpad-panel-SMFZ5eRQ.js} +1 -1
  36. package/dist/assets/{secrets-panel-Br6CcsOE.js → secrets-panel-BaEqnh6m.js} +1 -1
  37. package/dist/assets/{session-panel-JOOuJNOH.js → session-panel-CR_CZBSy.js} +1 -1
  38. package/dist/assets/table-C8uQmBAN.js +1 -0
  39. package/dist/assets/{terminal-DNwT6UrR.js → terminal-C7HXI-7B.js} +1 -1
  40. package/dist/assets/{tree-BdwmBGSx.js → tree-B1vM35Zj.js} +1 -1
  41. package/dist/assets/{useAddCell-BMDEXuVk.js → useAddCell-DRmuczCx.js} +1 -1
  42. package/dist/assets/{useCellActionButton-BDMlZzyv.js → useCellActionButton-DwRoApVS.js} +1 -1
  43. package/dist/assets/{useDeleteCell-BrMXAFkS.js → useDeleteCell-CR3IczUk.js} +1 -1
  44. package/dist/assets/{useDependencyPanelTab-B63bb4YX.js → useDependencyPanelTab-CLgnO1zH.js} +1 -1
  45. package/dist/assets/useNotebookActions-DhF-uJ0P.js +1 -0
  46. package/dist/assets/{useSplitCell-Cb0lf5MV.js → useSplitCell-IQsKBoRj.js} +1 -1
  47. package/dist/assets/{utilities.esm-BVFPJPyV.js → utilities.esm-DyYLtC1k.js} +2 -2
  48. package/dist/index.html +20 -20
  49. package/package.json +1 -1
  50. package/src/components/data-table/TableActions.tsx +5 -3
  51. package/src/components/data-table/download-actions.tsx +7 -2
  52. package/src/components/data-table/pagination.tsx +4 -4
  53. package/src/components/debug/indicator.tsx +1 -1
  54. package/src/components/editor/actions/useNotebookActions.tsx +4 -2
  55. package/src/components/editor/chrome/panels/context-aware-panel/context-aware-panel.tsx +1 -1
  56. package/src/components/editor/chrome/wrapper/app-chrome.tsx +6 -4
  57. package/src/components/editor/chrome/wrapper/footer-items/lsp-status.tsx +178 -0
  58. package/src/components/editor/chrome/wrapper/footer.tsx +1 -1
  59. package/src/components/editor/chrome/wrapper/sidebar.tsx +1 -1
  60. package/src/components/editor/controls/Controls.tsx +2 -2
  61. package/src/components/editor/controls/notebook-menu-dropdown.tsx +1 -1
  62. package/src/components/editor/file-tree/file-explorer.tsx +1 -1
  63. package/src/components/editor/header/status.tsx +1 -1
  64. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +13 -4
  65. package/src/components/home/components.tsx +1 -1
  66. package/src/components/static-html/static-banner.tsx +1 -1
  67. package/src/components/ui/dropdown-menu.tsx +1 -1
  68. package/src/components/ui/table.tsx +1 -1
  69. package/src/core/export/__tests__/hooks.test.ts +60 -58
  70. package/src/core/export/hooks.ts +71 -31
  71. package/src/core/network/types.ts +4 -0
  72. package/src/css/app/print.css +0 -14
  73. package/src/utils/__tests__/async-capture-tracker.test.ts +353 -0
  74. package/src/utils/__tests__/download.test.tsx +5 -114
  75. package/src/utils/async-capture-tracker.ts +168 -0
  76. package/src/utils/download.ts +17 -57
  77. package/src/utils/html-to-image.ts +9 -12
  78. package/dist/assets/download-CzPV-R6Z.js +0 -6
  79. package/dist/assets/edit-page-DFpXAt-U.js +0 -12
  80. package/dist/assets/home-page-DBWXVxWa.js +0 -4
  81. package/dist/assets/hooks-CpjzmBkw.js +0 -1
  82. package/dist/assets/run-page-BCOAGhl5.js +0 -1
  83. package/dist/assets/table-BSASHvkq.js +0 -1
  84. package/dist/assets/useNotebookActions-Cj4FtiIb.js +0 -1
@@ -166,51 +166,6 @@ describe("getImageDataUrlForCell", () => {
166
166
  );
167
167
  });
168
168
 
169
- it("should add printing classes before capture when snappy is false", async () => {
170
- vi.mocked(toPng).mockImplementation(async () => {
171
- // Check classes are applied during capture
172
- expect(mockElement.classList.contains("printing-output")).toBe(true);
173
- expect(document.body.classList.contains("printing")).toBe(true);
174
- expect(mockElement.style.overflow).toBe("auto");
175
- return mockDataUrl;
176
- });
177
-
178
- await getImageDataUrlForCell("cell-1" as CellId, false);
179
- });
180
-
181
- it("should remove printing classes after capture when snappy is false", async () => {
182
- vi.mocked(toPng).mockResolvedValue(mockDataUrl);
183
-
184
- await getImageDataUrlForCell("cell-1" as CellId, false);
185
-
186
- expect(mockElement.classList.contains("printing-output")).toBe(false);
187
- expect(document.body.classList.contains("printing")).toBe(false);
188
- });
189
-
190
- it("should add printing-output but NOT body.printing when snappy is true", async () => {
191
- vi.mocked(toPng).mockImplementation(async () => {
192
- // printing-output should still be added to the element
193
- expect(mockElement.classList.contains("printing-output")).toBe(true);
194
- // but body.printing should NOT be added when snappy mode is on
195
- expect(document.body.classList.contains("printing")).toBe(false);
196
- expect(mockElement.style.overflow).toBe("auto");
197
- return mockDataUrl;
198
- });
199
-
200
- await getImageDataUrlForCell("cell-1" as CellId, true);
201
- });
202
-
203
- it("should cleanup printing-output when snappy is true", async () => {
204
- mockElement.style.overflow = "hidden";
205
- vi.mocked(toPng).mockResolvedValue(mockDataUrl);
206
-
207
- await getImageDataUrlForCell("cell-1" as CellId, true);
208
-
209
- expect(mockElement.classList.contains("printing-output")).toBe(false);
210
- expect(document.body.classList.contains("printing")).toBe(false);
211
- expect(mockElement.style.overflow).toBe("hidden");
212
- });
213
-
214
169
  it("should restore original overflow style after capture", async () => {
215
170
  mockElement.style.overflow = "hidden";
216
171
  vi.mocked(toPng).mockResolvedValue(mockDataUrl);
@@ -234,88 +189,27 @@ describe("getImageDataUrlForCell", () => {
234
189
 
235
190
  await expect(getImageDataUrlForCell("cell-1" as CellId)).rejects.toThrow();
236
191
 
237
- expect(mockElement.classList.contains("printing-output")).toBe(false);
238
192
  expect(document.body.classList.contains("printing")).toBe(false);
239
193
  expect(mockElement.style.overflow).toBe("scroll");
240
194
  });
241
195
 
242
- it("should maintain body.printing during concurrent captures when snappy is false", async () => {
243
- // Create a second element
244
- const mockElement2 = document.createElement("div");
245
- mockElement2.id = CellOutputId.create("cell-2" as CellId);
246
- document.body.append(mockElement2);
247
-
248
- // Track body.printing state during each capture
249
- const printingStateDuringCaptures: boolean[] = [];
250
- let resolveFirst: () => void;
251
- let resolveSecond: () => void;
252
-
253
- const firstPromise = new Promise<void>((resolve) => {
254
- resolveFirst = resolve;
255
- });
256
- const secondPromise = new Promise<void>((resolve) => {
257
- resolveSecond = resolve;
258
- });
259
-
260
- vi.mocked(toPng).mockImplementation(async (element) => {
261
- printingStateDuringCaptures.push(
262
- document.body.classList.contains("printing"),
263
- );
264
-
265
- // Simulate async work - first capture takes longer
266
- await (element.id.includes("cell-1") ? firstPromise : secondPromise);
267
-
268
- // Check state again after waiting
269
- printingStateDuringCaptures.push(
270
- document.body.classList.contains("printing"),
271
- );
272
-
273
- return mockDataUrl;
274
- });
275
-
276
- // Start both captures concurrently with snappy = false (body.printing should be added)
277
- const capture1 = getImageDataUrlForCell("cell-1" as CellId, false);
278
- const capture2 = getImageDataUrlForCell("cell-2" as CellId, false);
279
-
280
- // Let second capture complete first
281
- resolveSecond!();
282
- await new Promise((r) => setTimeout(r, 0));
283
-
284
- // body.printing should still be present because cell-1 is still capturing
285
- expect(document.body.classList.contains("printing")).toBe(true);
286
-
287
- // Now let first capture complete
288
- resolveFirst!();
289
- await Promise.all([capture1, capture2]);
290
-
291
- // After all captures complete, body.printing should be removed
292
- expect(document.body.classList.contains("printing")).toBe(false);
293
-
294
- // All captures should have seen body.printing = true
295
- expect(printingStateDuringCaptures.every(Boolean)).toBe(true);
296
-
297
- mockElement2.remove();
298
- });
299
-
300
- it("should not interfere with body.printing during concurrent captures when snappy is true", async () => {
196
+ it("should handle concurrent captures correctly", async () => {
301
197
  // Create a second element
302
198
  const mockElement2 = document.createElement("div");
303
199
  mockElement2.id = CellOutputId.create("cell-2" as CellId);
304
200
  document.body.append(mockElement2);
305
201
 
306
202
  vi.mocked(toPng).mockImplementation(async () => {
307
- // body.printing should never be added when snappy is true
203
+ // body.printing should not be added during cell captures
308
204
  expect(document.body.classList.contains("printing")).toBe(false);
309
205
  return mockDataUrl;
310
206
  });
311
207
 
312
- // Start both captures concurrently with snappy = true (body.printing should NOT be added)
313
- const capture1 = getImageDataUrlForCell("cell-1" as CellId, true);
314
- const capture2 = getImageDataUrlForCell("cell-2" as CellId, true);
208
+ const capture1 = getImageDataUrlForCell("cell-1" as CellId);
209
+ const capture2 = getImageDataUrlForCell("cell-2" as CellId);
315
210
 
316
211
  await Promise.all([capture1, capture2]);
317
212
 
318
- // body.printing should still not be present
319
213
  expect(document.body.classList.contains("printing")).toBe(false);
320
214
 
321
215
  mockElement2.remove();
@@ -515,9 +409,7 @@ describe("downloadCellOutputAsImage", () => {
515
409
  it("should apply cell-specific preparation", async () => {
516
410
  vi.mocked(toPng).mockImplementation(async () => {
517
411
  // Check that cell-specific classes are applied
518
- expect(mockElement.classList.contains("printing-output")).toBe(true);
519
- expect(document.body.classList.contains("printing")).toBe(true);
520
- expect(mockElement.style.overflow).toBe("auto");
412
+ expect(mockElement.style.overflow).toBe("visible");
521
413
  return mockDataUrl;
522
414
  });
523
415
 
@@ -530,7 +422,6 @@ describe("downloadCellOutputAsImage", () => {
530
422
 
531
423
  await downloadCellOutputAsImage("cell-1" as CellId, "result");
532
424
 
533
- expect(mockElement.classList.contains("printing-output")).toBe(false);
534
425
  expect(document.body.classList.contains("printing")).toBe(false);
535
426
  expect(mockElement.style.overflow).toBe("visible");
536
427
  });
@@ -0,0 +1,168 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { Deferred } from "./Deferred";
4
+
5
+ /**
6
+ * Handle returned by {@link AsyncCaptureTracker.startCapture}.
7
+ *
8
+ * The handle is scoped to one capture attempt. If the same key is re-captured
9
+ * before this handle completes, calling `markCaptured` or `markFailed` on
10
+ * the stale handle is a safe no-op.
11
+ */
12
+ export interface CaptureHandle<R> {
13
+ /** Per-key AbortSignal — check between async steps. */
14
+ readonly signal: AbortSignal;
15
+ /** Mark the capture as successful and resolve waiters with the result. */
16
+ markCaptured(result: R): void;
17
+ /** Mark the capture as failed — the key returns to idle for retry. */
18
+ markFailed(): void;
19
+ }
20
+
21
+ interface InFlightEntry<R> {
22
+ controller: AbortController;
23
+ inputValue: unknown;
24
+ deferred: Deferred<R | undefined>;
25
+ }
26
+
27
+ /**
28
+ * Tracks async capture operations to prevent race conditions.
29
+ *
30
+ * Each key transitions through states:
31
+ * - **idle**: Not captured or previously failed — eligible for capture
32
+ * - **in-flight**: Capture started but not completed — skipped unless value changed
33
+ * - **captured(value)**: Successfully captured — skipped until value changes
34
+ *
35
+ * Abort is per-key: aborting one key's in-flight capture does not affect others.
36
+ *
37
+ * Guarantees:
38
+ * - Items are only marked "captured" after successful async completion
39
+ * - In-flight items with the same value are skipped (prevents duplicates)
40
+ * - In-flight items whose value changed are aborted and re-captured
41
+ * - Failed items return to idle (retried next cycle)
42
+ * - Concurrent callers can await an in-flight capture via {@link waitForInFlight}
43
+ * - Stale handles are safe no-ops (checked via entry identity)
44
+ */
45
+ export class AsyncCaptureTracker<K, R = unknown> {
46
+ /** Input values for successfully captured keys */
47
+ private capturedInputs = new Map<K, unknown>();
48
+ /** Per-key in-flight state */
49
+ private inFlight = new Map<K, InFlightEntry<R>>();
50
+
51
+ /** Abort an in-flight entry and resolve its waiters with `undefined`. */
52
+ private cancelEntry(entry: InFlightEntry<R>): void {
53
+ entry.controller.abort();
54
+ if (entry.deferred.status === "pending") {
55
+ entry.deferred.resolve(undefined);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Whether a key needs capturing based on its current input value.
61
+ * Returns false if:
62
+ * - Already captured with the same value
63
+ * - In-flight with the same value (let it finish — use {@link waitForInFlight} to get the result)
64
+ * Returns true if:
65
+ * - Never captured
66
+ * - Captured with a different value
67
+ * - In-flight with a different value (will be aborted on {@link startCapture})
68
+ */
69
+ needsCapture(key: K, inputValue: unknown): boolean {
70
+ if (this.capturedInputs.get(key) === inputValue) {
71
+ return false;
72
+ }
73
+ const flight = this.inFlight.get(key);
74
+ if (flight && flight.inputValue === inputValue) {
75
+ return false;
76
+ }
77
+ return true;
78
+ }
79
+
80
+ /**
81
+ * If the key is in-flight with the given input value, returns a promise
82
+ * that resolves when the capture completes (with the result, or `undefined`
83
+ * on failure/abort). Returns `null` otherwise.
84
+ */
85
+ waitForInFlight(key: K, inputValue: unknown): Promise<R | undefined> | null {
86
+ const flight = this.inFlight.get(key);
87
+ if (flight && flight.inputValue === inputValue) {
88
+ return flight.deferred.promise;
89
+ }
90
+ return null;
91
+ }
92
+
93
+ /**
94
+ * Start capturing a single key. If the key is already in-flight,
95
+ * aborts only that key's previous capture and resolves its waiters
96
+ * with `undefined`.
97
+ *
98
+ * @returns A {@link CaptureHandle} scoped to this attempt.
99
+ */
100
+ startCapture(key: K, inputValue: unknown): CaptureHandle<R> {
101
+ const prev = this.inFlight.get(key);
102
+ if (prev) {
103
+ this.cancelEntry(prev);
104
+ }
105
+
106
+ const controller = new AbortController();
107
+ const deferred = new Deferred<R | undefined>();
108
+ const entry: InFlightEntry<R> = { controller, inputValue, deferred };
109
+ this.inFlight.set(key, entry);
110
+
111
+ return {
112
+ signal: controller.signal,
113
+ markCaptured: (result: R) => {
114
+ // No-op if this handle was superseded by a newer startCapture
115
+ if (this.inFlight.get(key) !== entry) {
116
+ return;
117
+ }
118
+ deferred.resolve(result);
119
+ this.capturedInputs.set(key, inputValue);
120
+ this.inFlight.delete(key);
121
+ },
122
+ markFailed: () => {
123
+ if (this.inFlight.get(key) !== entry) {
124
+ return;
125
+ }
126
+ deferred.resolve(undefined);
127
+ this.inFlight.delete(key);
128
+ },
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Remove tracking for keys not in the given set.
134
+ * Aborts in-flight captures and resolves their waiters with `undefined`.
135
+ */
136
+ prune(currentKeys: Set<K>): void {
137
+ for (const key of this.capturedInputs.keys()) {
138
+ if (!currentKeys.has(key)) {
139
+ this.capturedInputs.delete(key);
140
+ }
141
+ }
142
+ for (const [key, entry] of this.inFlight) {
143
+ if (!currentKeys.has(key)) {
144
+ this.cancelEntry(entry);
145
+ this.inFlight.delete(key);
146
+ }
147
+ }
148
+ }
149
+
150
+ /** Whether any captures are currently in-flight. */
151
+ get isCapturing(): boolean {
152
+ return this.inFlight.size > 0;
153
+ }
154
+
155
+ /** Abort all in-flight captures. Resolves all waiters with `undefined`. */
156
+ abort(): void {
157
+ for (const entry of this.inFlight.values()) {
158
+ this.cancelEntry(entry);
159
+ }
160
+ this.inFlight.clear();
161
+ }
162
+
163
+ /** Reset all state. */
164
+ reset(): void {
165
+ this.abort();
166
+ this.capturedInputs.clear();
167
+ }
168
+ }
@@ -46,50 +46,21 @@ function findElementForCell(cellId: CellId): HTMLElement | undefined {
46
46
  return element;
47
47
  }
48
48
 
49
- /**
50
- * Reference counter for body.printing class to handle concurrent screenshot captures.
51
- * Only adds the class when count goes 0→1, only removes when count goes 1→0.
52
- */
53
- let bodyPrintingRefCount = 0;
54
-
55
- function acquireBodyPrinting() {
56
- bodyPrintingRefCount++;
57
- if (bodyPrintingRefCount === 1) {
58
- document.body.classList.add("printing");
59
- }
60
- }
61
-
62
- function releaseBodyPrinting() {
63
- bodyPrintingRefCount--;
64
- if (bodyPrintingRefCount === 0) {
65
- document.body.classList.remove("printing");
66
- }
67
- }
68
-
69
49
  /**
70
50
  * Prepare a cell element for screenshot capture.
71
51
  *
72
52
  * @param element - The cell output element to prepare
73
- * @param snappy - When true, avoids layout shifts and speeds up the capture.
74
53
  * @returns A cleanup function to restore the element's original state
75
54
  */
76
- function prepareCellElementForScreenshot(
77
- element: HTMLElement,
78
- snappy: boolean,
79
- ) {
80
- element.classList.add("printing-output");
81
- if (!snappy) {
82
- acquireBodyPrinting();
83
- }
55
+ function prepareCellElementForScreenshot(element: HTMLElement) {
84
56
  const originalOverflow = element.style.overflow;
85
- element.style.overflow = "auto";
57
+ const maxHeight = element.style.maxHeight;
58
+ element.style.overflow = "visible";
59
+ element.style.maxHeight = "none";
86
60
 
87
61
  return () => {
88
- element.classList.remove("printing-output");
89
- if (!snappy) {
90
- releaseBodyPrinting();
91
- }
92
62
  element.style.overflow = originalOverflow;
63
+ element.style.maxHeight = maxHeight;
93
64
  };
94
65
  }
95
66
 
@@ -99,12 +70,10 @@ const THRESHOLD_TIME_MS = 500;
99
70
  * Capture a cell output as a PNG data URL.
100
71
  *
101
72
  * @param cellId - The ID of the cell to capture
102
- * @param snappy - When true, uses a faster method to capture the image. Avoids layout shifts.
103
73
  * @returns The PNG as a data URL, or undefined if the cell element wasn't found
104
74
  */
105
75
  export async function getImageDataUrlForCell(
106
76
  cellId: CellId,
107
- snappy = false,
108
77
  ): Promise<string | undefined> {
109
78
  const element = findElementForCell(cellId);
110
79
  if (!element) {
@@ -116,11 +85,11 @@ export async function getImageDataUrlForCell(
116
85
  return iframeDataUrl;
117
86
  }
118
87
 
119
- const cleanup = prepareCellElementForScreenshot(element, snappy);
88
+ const cleanup = prepareCellElementForScreenshot(element);
120
89
 
121
90
  try {
122
91
  const startTime = Date.now();
123
- const dataUrl = await toPng(element, undefined, snappy);
92
+ const dataUrl = await toPng(element);
124
93
  const timeTaken = Date.now() - startTime;
125
94
  if (timeTaken > THRESHOLD_TIME_MS) {
126
95
  Logger.debug(
@@ -142,25 +111,20 @@ export async function downloadCellOutputAsImage(
142
111
  cellId: CellId,
143
112
  filename: string,
144
113
  ) {
145
- const element = findElementForCell(cellId);
146
- if (!element) {
147
- return;
148
- }
149
-
150
- // Cell outputs that are iframes
151
- const iframeDataUrl = await captureIframeAsImage(element);
152
- if (iframeDataUrl) {
153
- downloadByURL(iframeDataUrl, Filenames.toPNG(filename));
114
+ const dataUrl = await getImageDataUrlForCell(cellId);
115
+ if (!dataUrl) {
154
116
  return;
155
117
  }
156
-
157
- await downloadHTMLAsImage({
158
- element,
159
- filename,
160
- prepare: () => prepareCellElementForScreenshot(element, false),
161
- });
118
+ return downloadByURL(dataUrl, Filenames.toPNG(filename));
162
119
  }
163
120
 
121
+ export const ADD_PRINTING_CLASS = (): (() => void) => {
122
+ document.body.classList.add("printing");
123
+ return () => {
124
+ document.body.classList.remove("printing");
125
+ };
126
+ };
127
+
164
128
  export async function downloadHTMLAsImage(opts: {
165
129
  element: HTMLElement;
166
130
  filename: string;
@@ -175,10 +139,6 @@ export async function downloadHTMLAsImage(opts: {
175
139
  let cleanup: (() => void) | undefined;
176
140
  if (prepare) {
177
141
  cleanup = prepare(element);
178
- } else {
179
- // When no prepare function is provided (e.g., downloading full notebook),
180
- // add body.printing ourselves
181
- document.body.classList.add("printing");
182
142
  }
183
143
 
184
144
  try {
@@ -113,10 +113,9 @@ export const necessaryStyleProperties = [
113
113
  "clip-path",
114
114
 
115
115
  // Overflow & Visibility
116
- // We don't include overflow properties because they can include scrollbars
117
- // "overflow",
118
- // "overflow-x",
119
- // "overflow-y",
116
+ "overflow",
117
+ "overflow-x",
118
+ "overflow-y",
120
119
  "visibility",
121
120
 
122
121
  // SVG
@@ -149,12 +148,11 @@ export const defaultHtmlToImageOptions: HtmlToImageOptions = {
149
148
  filter: (node) => {
150
149
  try {
151
150
  if ("classList" in node) {
152
- // Filter out matplotlib toolbars
153
- if (node.classList.contains("mpl-toolbar")) {
154
- return false;
155
- }
156
-
157
- if (node.classList.contains("no-print")) {
151
+ const classes = node.classList;
152
+ if (
153
+ classes.contains("mpl-toolbar") ||
154
+ classes.contains("print:hidden")
155
+ ) {
158
156
  return false;
159
157
  }
160
158
  }
@@ -167,6 +165,7 @@ export const defaultHtmlToImageOptions: HtmlToImageOptions = {
167
165
  onImageErrorHandler: (event) => {
168
166
  Logger.error("Error loading image:", event);
169
167
  },
168
+ includeStyleProperties: necessaryStyleProperties,
170
169
  };
171
170
 
172
171
  /**
@@ -176,11 +175,9 @@ export const defaultHtmlToImageOptions: HtmlToImageOptions = {
176
175
  export async function toPng(
177
176
  element: HTMLElement,
178
177
  options?: HtmlToImageOptions,
179
- snappy?: boolean,
180
178
  ): Promise<string> {
181
179
  return htmlToImageToPng(element, {
182
180
  ...defaultHtmlToImageOptions,
183
- includeStyleProperties: snappy ? necessaryStyleProperties : undefined,
184
181
  ...options,
185
182
  });
186
183
  }
@@ -1,6 +0,0 @@
1
- var ce=Object.defineProperty;var de=(r,e,t)=>e in r?ce(r,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):r[e]=t;var O=(r,e,t)=>de(r,typeof e!="symbol"?e+"":e,t);import{s as V,t as G}from"./chunk-LvLJmgfZ.js";import{t as q}from"./react-BGmjiNul.js";import{ii as fe,zt as me}from"./cells-BW_4R0Qw.js";import{t as B}from"./compiler-runtime-DeeZ7FnK.js";import{d as W}from"./hotkeys-BHHWjLlp.js";import{t as pe}from"./jsx-runtime-ZmTK25f3.js";import{t as z}from"./cn-BKtXLv3a.js";import{t as ve}from"./requests-BsVD4CdD.js";import{t as he}from"./createLucideIcon-CnW3RofX.js";import{t as _}from"./use-toast-rmUWldD_.js";import{d as ge}from"./popover-D16ZremR.js";import{r as ye}from"./errors-2SszdW9t.js";import{t as H}from"./dist-CdxIjAOP.js";import{t as k}from"./html-to-image-DKvXQkl5.js";var we=he("chevron-left",[["path",{d:"m15 18-6-6 6-6",key:"1wnfg3"}]]),X=r=>{let e,t=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),t.forEach(u=>u(e,d))}},n=()=>e,i={setState:a,getState:n,getInitialState:()=>o,subscribe:l=>(t.add(l),()=>t.delete(l)),destroy:()=>{t.clear()}},o=e=r(a,n,i);return i},xe=r=>r?X(r):X,be=G((r=>{var e=q(),t=ge();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=t.useSyncExternalStore,o=e.useRef,l=e.useEffect,s=e.useMemo,c=e.useDebugValue;r.useSyncExternalStoreWithSelector=function(d,u,f,g,m){var p=o(null);if(p.current===null){var x={hasValue:!1,value:null};p.current=x}else x=p.current;p=s(function(){function U(y){if(!F){if(F=!0,$=y,y=g(y),m!==void 0&&x.hasValue){var N=x.value;if(m(N,y))return L=N}return L=y}if(N=L,n($,y))return N;var C=g(y);return m!==void 0&&m(N,C)?($=y,N):($=y,L=C)}var F=!1,$,L,I=f===void 0?null:f;return[function(){return U(u())},I===null?void 0:function(){return U(I())}]},[u,f,g,m]);var b=i(d,p[0],p[1]);return l(function(){x.hasValue=!0,x.value=b},[b]),c(b),b}})),Ne=G(((r,e)=>{e.exports=be()}));const h={toMarkdown:r=>h.replace(r,"md"),toHTML:r=>h.replace(r,"html"),toPNG:r=>h.replace(r,"png"),toPDF:r=>h.replace(r,"pdf"),toPY:r=>h.replace(r,"py"),withoutExtension:r=>{let e=r.split(".");return e.length===1?r:e.slice(0,-1).join(".")},replace:(r,e)=>r.endsWith(`.${e}`)?r:`${h.withoutExtension(r)}.${e}`};var P=320,S=180;function Y(r){if(!r||r==="about:blank")return null;try{let e=new URL(r,window.location.href);return e.origin===window.location.origin?null:e.href}catch{return r}}function Pe(r,e,t){let a=[],n="";for(let i of e){let o=n+i;r.measureText(o).width<=t?n=o:(n&&a.push(n),n=i)}return n&&a.push(n),a}function j(r){let e=window.devicePixelRatio||1,t=document.createElement("canvas");t.width=P*e,t.height=S*e;let a=t.getContext("2d");if(!a)return t.toDataURL("image/png");a.scale(e,e),a.fillStyle="#f3f4f6",a.fillRect(0,0,P,S),a.strokeStyle="#d1d5db",a.strokeRect(.5,.5,P-1,S-1),a.fillStyle="#6b7280",a.font="8px sans-serif",a.textAlign="center",a.textBaseline="middle";let n=P-32,i=r?Pe(a,r,n):[],o=(S-(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 t.toDataURL("image/png")}async function J(r){var n;let e=r.querySelector("iframe");if(!e)return null;let t=Y(e.getAttribute("src"));if(t)return j(t);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 j(null)}for(let i of a.querySelectorAll("iframe")){let o=Y(i.getAttribute("src"));if(o)return j(o)}try{return await k(a.body)}catch{return j(null)}}var K=class ue{constructor(e){O(this,"progress",0);O(this,"listeners",new Set);this.total=e}static indeterminate(){return new ue("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 t of this.listeners)t(e)}},v=V(q(),1),w=V(pe(),1);function $e(r,e=[]){let t=[];function a(i,o){let l=v.createContext(o);l.displayName=i+"Context";let s=t.length;t=[...t,o];let c=u=>{var b;let{scope:f,children:g,...m}=u,p=((b=f==null?void 0:f[r])==null?void 0:b[s])||l,x=v.useMemo(()=>m,Object.values(m));return(0,w.jsx)(p.Provider,{value:x,children:g})};c.displayName=i+"Provider";function d(u,f){var p;let g=((p=f==null?void 0:f[r])==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=t.map(o=>v.createContext(o));return function(o){let l=(o==null?void 0:o[r])||i;return v.useMemo(()=>({[`__scope${r}`]:{...o,[r]:l}}),[o,l])}};return n.scopeName=r,[a,Le(n,...e)]}function Le(...r){let e=r[0];if(r.length===1)return e;let t=()=>{let a=r.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 t.scopeName=e.scopeName,t}var M="Progress",T=100,[Se,at]=$e(M),[je,Ee]=Se(M),Q=v.forwardRef((r,e)=>{let{__scopeProgress:t,value:a=null,max:n,getValueLabel:i=Re,...o}=r;(n||n===0)&&!re(n)&&console.error(De(`${n}`,"Progress"));let l=re(n)?n:T;a!==null&&!ne(a,l)&&console.error(Oe(`${a}`,"Progress"));let s=ne(a,l)?a:null,c=E(s)?i(s,l):void 0;return(0,w.jsx)(je,{scope:t,value:s,max:l,children:(0,w.jsx)(H.div,{"aria-valuemax":l,"aria-valuemin":0,"aria-valuenow":E(s)?s:void 0,"aria-valuetext":c,role:"progressbar","data-state":te(s,l),"data-value":s??void 0,"data-max":l,...o,ref:e})})});Q.displayName=M;var Z="ProgressIndicator",ee=v.forwardRef((r,e)=>{let{__scopeProgress:t,...a}=r,n=Ee(Z,t);return(0,w.jsx)(H.div,{"data-state":te(n.value,n.max),"data-value":n.value??void 0,"data-max":n.max,...a,ref:e})});ee.displayName=Z;function Re(r,e){return`${Math.round(r/e*100)}%`}function te(r,e){return r==null?"indeterminate":r===e?"complete":"loading"}function E(r){return typeof r=="number"}function re(r){return E(r)&&!isNaN(r)&&r>0}function ne(r,e){return E(r)&&!isNaN(r)&&r<=e&&r>=0}function De(r,e){return`Invalid prop \`max\` of value \`${r}\` supplied to \`${e}\`. Only numbers greater than 0 are valid max values. Defaulting to \`${T}\`.`}function Oe(r,e){return`Invalid prop \`value\` of value \`${r}\` supplied to \`${e}\`. The \`value\` prop must be:
2
- - a positive number
3
- - less than the value passed to \`max\` (or ${T} if no \`max\` prop is set)
4
- - \`null\` or \`undefined\` if the progress is indeterminate.
5
-
6
- Defaulting to \`null\`.`}var ae=Q,_e=ee,ke=B(),A=v.forwardRef((r,e)=>{let t=(0,ke.c)(20),a,n,i,o;t[0]===r?(a=t[1],n=t[2],i=t[3],o=t[4]):({className:a,value:o,indeterminate:n,...i}=r,t[0]=r,t[1]=a,t[2]=n,t[3]=i,t[4]=o);let l;t[5]===a?l=t[6]:(l=z("relative h-2 w-full overflow-hidden rounded-full bg-primary/20",a),t[5]=a,t[6]=l);let s=n?"w-1/3 animate-progress-indeterminate":"w-full transition-transform duration-300 ease-out",c;t[7]===s?c=t[8]:(c=z("h-full flex-1 bg-primary",s),t[7]=s,t[8]=c);let d;t[9]!==n||t[10]!==o?(d=n?void 0:{transform:`translateX(-${100-(o||0)}%)`},t[9]=n,t[10]=o,t[11]=d):d=t[11];let u;t[12]!==c||t[13]!==d?(u=(0,w.jsx)(_e,{className:c,style:d}),t[12]=c,t[13]=d,t[14]=u):u=t[14];let f;return t[15]!==i||t[16]!==e||t[17]!==l||t[18]!==u?(f=(0,w.jsx)(ae,{ref:e,className:l,...i,children:u}),t[15]=i,t[16]=e,t[17]=l,t[18]=u,t[19]=f):f=t[19],f});A.displayName=ae.displayName;var Me=B();const Te=r=>{let e=(0,Me.c)(13),{progress:t,showPercentage:a}=r,n=a===void 0?!1:a,i,o;e[0]===t?(i=e[1],o=e[2]):(i=g=>t.subscribe(g),o=()=>t.getProgress(),e[0]=t,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,w.jsx)(A,{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,w.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,w.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 Ae(r,e){let t=K.indeterminate(),a=_({title:r,description:v.createElement(Te,{progress:t}),duration:1/0});try{let n=await e(t);return a.dismiss(),n}catch(n){throw a.dismiss(),n}}function oe(r){let e=document.getElementById(fe.create(r));if(!e){W.error(`Output element not found for cell ${r}`);return}return e}var R=0;function Ue(){R++,R===1&&document.body.classList.add("printing")}function Fe(){R--,R===0&&document.body.classList.remove("printing")}function ie(r,e){r.classList.add("printing-output"),e||Ue();let t=r.style.overflow;return r.style.overflow="auto",()=>{r.classList.remove("printing-output"),e||Fe(),r.style.overflow=t}}var Ie=500;async function Ce(r,e=!1){let t=oe(r);if(!t)return;let a=await J(t);if(a)return a;let n=ie(t,e);try{let i=Date.now(),o=await k(t,void 0,e),l=Date.now()-i;return l>Ie&&W.debug("toPng operation for element",t,`took ${l} ms (exceeds threshold)`),o}finally{n()}}async function Ve(r,e){let t=oe(r);if(!t)return;let a=await J(t);if(a){D(a,h.toPNG(e));return}await le({element:t,filename:e,prepare:()=>ie(t,!1)})}async function le(r){let{element:e,filename:t,prepare:a}=r,n=document.getElementById("App"),i=(n==null?void 0:n.scrollTop)??0,o;a?o=a(e):document.body.classList.add("printing");try{D(await k(e),h.toPNG(t))}catch{_({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 D(r,e){let t=document.createElement("a");t.href=r,t.download=e,t.click(),t.remove()}function se(r,e){let t=URL.createObjectURL(r);D(t,e),URL.revokeObjectURL(t)}async function Ge(r){let e=ve(),{filename:t,webpdf:a}=r;try{let n=await e.exportAsPDF({webpdf:a}),i=me.basename(t);se(n,h.toPDF(i))}catch(n){throw _({title:"Failed to download",description:ye(n),variant:"danger"}),n}}export{le as a,A as c,Ne as d,xe as f,Ve as i,K as l,se as n,Ce as o,we as p,D as r,Ae as s,Ge as t,h as u};