@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.
- package/dist/assets/{JsonOutput-BGSqWZkD.js → JsonOutput-DtidtKaJ.js} +2 -2
- package/dist/assets/{MarimoErrorOutput-DFV7O8tN.js → MarimoErrorOutput-Cci2wITc.js} +1 -1
- package/dist/assets/{add-cell-with-ai-gDjJxct8.js → add-cell-with-ai-DT1ae2MK.js} +1 -1
- package/dist/assets/{add-database-form-CqEjMrq2.js → add-database-form-_pTLtiHN.js} +1 -1
- package/dist/assets/{agent-panel-D_hcgMlT.js → agent-panel-oLGqa4bG.js} +1 -1
- package/dist/assets/{ai-model-dropdown-C_l_zAXd.js → ai-model-dropdown-BL-PaF8o.js} +1 -1
- package/dist/assets/{app-config-button-CM05VD_Y.js → app-config-button-BiwYEPE8.js} +1 -1
- package/dist/assets/{cell-editor-ihhw9Ql3.js → cell-editor-B3U1SnYJ.js} +1 -1
- package/dist/assets/{chat-display-yGAtJXm0.js → chat-display-B34MaCGM.js} +1 -1
- package/dist/assets/{chat-panel-D3T1CjpZ.js → chat-panel-fuQFRvFm.js} +1 -1
- package/dist/assets/{column-preview-DbqM1diq.js → column-preview-B-dViv1i.js} +1 -1
- package/dist/assets/{command-palette-C2L80RlN.js → command-palette-KuNgJNix.js} +1 -1
- package/dist/assets/{common-D32S2RVD.js → common-DxKcMlJZ.js} +1 -1
- package/dist/assets/{dependency-graph-panel-C7ZXgECY.js → dependency-graph-panel-BZEIOxVz.js} +1 -1
- package/dist/assets/download-Bwa9P-Pz.js +6 -0
- package/dist/assets/{dropdown-menu-df9T83C0.js → dropdown-menu-B-6unW-7.js} +1 -1
- package/dist/assets/edit-page-DBFIML4p.js +13 -0
- package/dist/assets/{error-panel-DKIWwbhe.js → error-panel-DulelhA-.js} +1 -1
- package/dist/assets/{file-explorer-panel-D4VHkjMW.js → file-explorer-panel-Dn9tKw3E.js} +1 -1
- package/dist/assets/{form-XjMNGyzu.js → form-B1n-e_X0.js} +1 -1
- package/dist/assets/{glide-data-editor-BBSxoBI-.js → glide-data-editor-HGkaxqOo.js} +1 -1
- package/dist/assets/home-page--XVUAUCM.js +4 -0
- package/dist/assets/hooks-B1nUQK2T.js +1 -0
- package/dist/assets/{html-to-image-DKvXQkl5.js → html-to-image-Cu1p0tCK.js} +2 -2
- package/dist/assets/{index-DmMvDRRC.css → index-Bj5F80Z9.css} +1 -1
- package/dist/assets/{index-BRCNo5ma.js → index-DTpRqi46.js} +5 -5
- package/dist/assets/{layout-DiDoTAUA.js → layout-KY92f2Sm.js} +3 -3
- package/dist/assets/{markdown-renderer-xQJ1KM4c.js → markdown-renderer-Dpn5NCvn.js} +1 -1
- package/dist/assets/{packages-panel-D53RuG3X.js → packages-panel-CBc59eNR.js} +1 -1
- package/dist/assets/{panels-BhznEx5N.js → panels-B0B71dYl.js} +1 -1
- package/dist/assets/{popover-D16ZremR.js → popover-Gz-GJzym.js} +1 -1
- package/dist/assets/{readonly-python-code-kImQwJ5f.js → readonly-python-code-CCwpyiLX.js} +1 -1
- package/dist/assets/{renderShortcut-DHc-p-_c.js → renderShortcut-DEwfrKeS.js} +1 -1
- package/dist/assets/run-page-Dug0EU2T.js +1 -0
- package/dist/assets/{scratchpad-panel-C8wx6cRl.js → scratchpad-panel-SMFZ5eRQ.js} +1 -1
- package/dist/assets/{secrets-panel-Br6CcsOE.js → secrets-panel-BaEqnh6m.js} +1 -1
- package/dist/assets/{session-panel-JOOuJNOH.js → session-panel-CR_CZBSy.js} +1 -1
- package/dist/assets/table-C8uQmBAN.js +1 -0
- package/dist/assets/{terminal-DNwT6UrR.js → terminal-C7HXI-7B.js} +1 -1
- package/dist/assets/{tree-BdwmBGSx.js → tree-B1vM35Zj.js} +1 -1
- package/dist/assets/{useAddCell-BMDEXuVk.js → useAddCell-DRmuczCx.js} +1 -1
- package/dist/assets/{useCellActionButton-BDMlZzyv.js → useCellActionButton-DwRoApVS.js} +1 -1
- package/dist/assets/{useDeleteCell-BrMXAFkS.js → useDeleteCell-CR3IczUk.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-B63bb4YX.js → useDependencyPanelTab-CLgnO1zH.js} +1 -1
- package/dist/assets/useNotebookActions-DhF-uJ0P.js +1 -0
- package/dist/assets/{useSplitCell-Cb0lf5MV.js → useSplitCell-IQsKBoRj.js} +1 -1
- package/dist/assets/{utilities.esm-BVFPJPyV.js → utilities.esm-DyYLtC1k.js} +2 -2
- package/dist/index.html +20 -20
- package/package.json +1 -1
- package/src/components/data-table/TableActions.tsx +5 -3
- package/src/components/data-table/download-actions.tsx +7 -2
- package/src/components/data-table/pagination.tsx +4 -4
- package/src/components/debug/indicator.tsx +1 -1
- package/src/components/editor/actions/useNotebookActions.tsx +4 -2
- package/src/components/editor/chrome/panels/context-aware-panel/context-aware-panel.tsx +1 -1
- package/src/components/editor/chrome/wrapper/app-chrome.tsx +6 -4
- package/src/components/editor/chrome/wrapper/footer-items/lsp-status.tsx +178 -0
- package/src/components/editor/chrome/wrapper/footer.tsx +1 -1
- package/src/components/editor/chrome/wrapper/sidebar.tsx +1 -1
- package/src/components/editor/controls/Controls.tsx +2 -2
- package/src/components/editor/controls/notebook-menu-dropdown.tsx +1 -1
- package/src/components/editor/file-tree/file-explorer.tsx +1 -1
- package/src/components/editor/header/status.tsx +1 -1
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +13 -4
- package/src/components/home/components.tsx +1 -1
- package/src/components/static-html/static-banner.tsx +1 -1
- package/src/components/ui/dropdown-menu.tsx +1 -1
- package/src/components/ui/table.tsx +1 -1
- package/src/core/export/__tests__/hooks.test.ts +60 -58
- package/src/core/export/hooks.ts +71 -31
- package/src/core/network/types.ts +4 -0
- package/src/css/app/print.css +0 -14
- package/src/utils/__tests__/async-capture-tracker.test.ts +353 -0
- package/src/utils/__tests__/download.test.tsx +5 -114
- package/src/utils/async-capture-tracker.ts +168 -0
- package/src/utils/download.ts +17 -57
- package/src/utils/html-to-image.ts +9 -12
- package/dist/assets/download-CzPV-R6Z.js +0 -6
- package/dist/assets/edit-page-DFpXAt-U.js +0 -12
- package/dist/assets/home-page-DBWXVxWa.js +0 -4
- package/dist/assets/hooks-CpjzmBkw.js +0 -1
- package/dist/assets/run-page-BCOAGhl5.js +0 -1
- package/dist/assets/table-BSASHvkq.js +0 -1
- 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
|
|
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
|
|
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
|
-
|
|
313
|
-
const
|
|
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.
|
|
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
|
+
}
|
package/src/utils/download.ts
CHANGED
|
@@ -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.
|
|
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
|
|
88
|
+
const cleanup = prepareCellElementForScreenshot(element);
|
|
120
89
|
|
|
121
90
|
try {
|
|
122
91
|
const startTime = Date.now();
|
|
123
|
-
const dataUrl = await toPng(element
|
|
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
|
|
146
|
-
if (!
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
153
|
-
if (
|
|
154
|
-
|
|
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};
|