@marimo-team/islands 0.19.5-dev40 → 0.19.5-dev44

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.
@@ -1,10 +1,12 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import { toPng } from "html-to-image";
3
3
  import { toast } from "@/components/ui/use-toast";
4
+ import { type CellId, CellOutputId } from "@/core/cells/ids";
4
5
  import { getRequestClient } from "@/core/network/requests";
5
6
  import { Filenames } from "@/utils/filenames";
6
7
  import { Paths } from "@/utils/paths";
7
8
  import { prettyError } from "./errors";
9
+ import { Logger } from "./Logger";
8
10
 
9
11
  /**
10
12
  * Show a loading toast while an async operation is in progress.
@@ -16,6 +18,7 @@ export async function withLoadingToast<T>(
16
18
  ): Promise<T> {
17
19
  const loadingToast = toast({
18
20
  title,
21
+ duration: Infinity,
19
22
  });
20
23
  try {
21
24
  const result = await fn();
@@ -27,15 +30,111 @@ export async function withLoadingToast<T>(
27
30
  }
28
31
  }
29
32
 
30
- export async function downloadHTMLAsImage(
31
- element: HTMLElement,
33
+ function findElementForCell(cellId: CellId): HTMLElement | undefined {
34
+ const element = document.getElementById(CellOutputId.create(cellId));
35
+ if (!element) {
36
+ Logger.error(`Output element not found for cell ${cellId}`);
37
+ return;
38
+ }
39
+ return element;
40
+ }
41
+
42
+ /**
43
+ * Reference counter for body.printing class to handle concurrent screenshot captures.
44
+ * Only adds the class when count goes 0→1, only removes when count goes 1→0.
45
+ */
46
+ let bodyPrintingRefCount = 0;
47
+
48
+ function acquireBodyPrinting() {
49
+ bodyPrintingRefCount++;
50
+ if (bodyPrintingRefCount === 1) {
51
+ document.body.classList.add("printing");
52
+ }
53
+ }
54
+
55
+ function releaseBodyPrinting() {
56
+ bodyPrintingRefCount--;
57
+ if (bodyPrintingRefCount === 0) {
58
+ document.body.classList.remove("printing");
59
+ }
60
+ }
61
+
62
+ /*
63
+ * Prepare a cell element for screenshot capture.
64
+ * Returns a cleanup function that should be called when the screenshot is complete.
65
+ */
66
+ function prepareCellElementForScreenshot(element: HTMLElement) {
67
+ element.classList.add("printing-output");
68
+ acquireBodyPrinting();
69
+ const originalOverflow = element.style.overflow;
70
+ element.style.overflow = "auto";
71
+
72
+ return () => {
73
+ element.classList.remove("printing-output");
74
+ releaseBodyPrinting();
75
+ element.style.overflow = originalOverflow;
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Capture a cell output as a PNG data URL.
81
+ */
82
+ export async function getImageDataUrlForCell(
83
+ cellId: CellId,
84
+ ): Promise<string | undefined> {
85
+ const element = findElementForCell(cellId);
86
+ if (!element) {
87
+ return;
88
+ }
89
+ const cleanup = prepareCellElementForScreenshot(element);
90
+
91
+ try {
92
+ return await toPng(element);
93
+ } finally {
94
+ cleanup();
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Download a cell output as a PNG image file.
100
+ */
101
+ export async function downloadCellOutputAsImage(
102
+ cellId: CellId,
32
103
  filename: string,
33
104
  ) {
105
+ const element = findElementForCell(cellId);
106
+ if (!element) {
107
+ return;
108
+ }
109
+
110
+ await downloadHTMLAsImage({
111
+ element,
112
+ filename,
113
+ prepare: prepareCellElementForScreenshot,
114
+ });
115
+ }
116
+
117
+ export async function downloadHTMLAsImage(opts: {
118
+ element: HTMLElement;
119
+ filename: string;
120
+ prepare?: (element: HTMLElement) => () => void;
121
+ }) {
122
+ const { element, filename, prepare } = opts;
123
+
34
124
  // Capture current scroll position
35
125
  const appEl = document.getElementById("App");
36
126
  const currentScrollY = appEl?.scrollTop ?? 0;
37
- // Add classnames for printing
38
- document.body.classList.add("printing");
127
+
128
+ let cleanup: (() => void) | undefined;
129
+ if (prepare) {
130
+ // Let the prepare function handle adding classes (e.g., body.printing)
131
+ cleanup = prepare(element);
132
+ } else {
133
+ // When no prepare function is provided (e.g., downloading full notebook),
134
+ // add body.printing ourselves
135
+ document.body.classList.add("printing");
136
+ }
137
+
39
138
  try {
40
139
  // Get screenshot
41
140
  const dataUrl = await toPng(element);
@@ -47,8 +146,10 @@ export async function downloadHTMLAsImage(
47
146
  variant: "danger",
48
147
  });
49
148
  } finally {
50
- // Remove classnames for printing
51
- document.body.classList.remove("printing");
149
+ cleanup?.();
150
+ if (document.body.classList.contains("printing")) {
151
+ document.body.classList.remove("printing");
152
+ }
52
153
  // Restore scroll position
53
154
  requestAnimationFrame(() => {
54
155
  appEl?.scrollTo(0, currentScrollY);
@@ -1,14 +0,0 @@
1
- /* Copyright 2026 Marimo. All rights reserved. */
2
-
3
- import { type CellId, CellOutputId } from "@/core/cells/ids";
4
- import { downloadHTMLAsImage } from "@/utils/download";
5
-
6
- export function downloadCellOutput(cellId: CellId) {
7
- const output = document.getElementById(CellOutputId.create(cellId));
8
- if (output) {
9
- output.classList.add("printing-output");
10
- downloadHTMLAsImage(output, "result.png").finally(() => {
11
- output.classList.remove("printing-output");
12
- });
13
- }
14
- }