@marimo-team/islands 0.24.1-dev87 → 0.24.1-dev90

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.
@@ -13,6 +13,9 @@ import { isStaticNotebook } from "@/core/static/static-state";
13
13
  import { createShareableLink } from "@/core/wasm/share";
14
14
  import { copyToClipboard } from "@/utils/copy";
15
15
  import { downloadBlob } from "@/utils/download";
16
+ import { Paths } from "@/utils/paths";
17
+ import { shellQuote } from "@/utils/shell";
18
+ import { MarimoPlusIcon } from "../icons/marimo-icons";
16
19
  import { Button } from "../ui/button";
17
20
  import {
18
21
  Dialog,
@@ -22,8 +25,8 @@ import {
22
25
  DialogTitle,
23
26
  DialogTrigger,
24
27
  } from "../ui/dialog";
28
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
25
29
  import { toast } from "../ui/use-toast";
26
- import { MarimoPlusIcon } from "../icons/marimo-icons";
27
30
 
28
31
  export const StaticBanner: React.FC = () => {
29
32
  const code = useAtomValue(codeAtom);
@@ -60,21 +63,14 @@ export const StaticBanner: React.FC = () => {
60
63
  };
61
64
 
62
65
  const StaticBannerDialog = ({ code }: { code: string }) => {
63
- let filename = useFilename() || "notebook.py";
64
- // Trim the path
65
- const lastSlash = filename.lastIndexOf("/");
66
- if (lastSlash !== -1) {
67
- filename = filename.slice(lastSlash + 1);
68
- }
66
+ const filename = Paths.basename(useFilename() || "notebook.py");
69
67
 
70
68
  const [resolvedConfig] = useResolvedMarimoConfig();
71
- const molabEnabled = resolvedConfig.sharing?.molab !== false;
69
+ const molabEnabled = resolvedConfig.sharing?.molab ?? true;
72
70
 
73
- const href = window.location.href;
74
- const molabLink = createShareableLink({
75
- code,
76
- baseUrl: `${Constants.molab}/new`,
77
- });
71
+ const runTarget = getStaticNotebookRunTarget(window.location.href, filename);
72
+ const runsFromUrl = runTarget !== filename;
73
+ const quotedRunTarget = shellQuote(runTarget);
78
74
 
79
75
  return (
80
76
  <Dialog>
@@ -90,62 +86,46 @@ const StaticBannerDialog = ({ code }: { code: string }) => {
90
86
  <DialogContent>
91
87
  <DialogHeader>
92
88
  <DialogTitle>{filename}</DialogTitle>
93
- <DialogDescription className="pt-3 text-left space-y-3">
94
- <p>
95
- This is a static{" "}
96
- <a
97
- href={Constants.githubPage}
98
- target="_blank"
99
- className="text-(--sky-11) hover:underline font-medium"
100
- >
101
- marimo
102
- </a>{" "}
103
- notebook. To run interactively:
104
- </p>
105
-
106
- <div className="rounded-lg p-3 border bg-(--sky-2) border-(--sky-7)">
107
- <div className="font-mono text-(--sky-11) leading-relaxed">
108
- pip install marimo
109
- <br />
110
- marimo edit {filename}
111
- </div>
112
- </div>
113
-
114
- {!href.endsWith(".html") && (
115
- <div className="rounded-lg p-3 border bg-(--sky-2) border-(--sky-7)">
116
- <div className="text-sm text-(--sky-12) mb-1">
117
- Or run directly from URL:
118
- </div>
119
- <div className="font-mono text-(--sky-11) break-all">
120
- marimo edit {window.location.href}
121
- </div>
122
- </div>
123
- )}
124
-
125
- {molabEnabled && (
126
- <div className="pt-3 border-t flex gap-2 items-center">
127
- <Button
128
- asChild={true}
129
- variant="outline"
130
- size="xs"
131
- className="shrink-0"
89
+ <DialogDescription asChild={true}>
90
+ <div className="pt-3 text-left space-y-3">
91
+ <p>
92
+ This is a static{" "}
93
+ <a
94
+ href={Constants.githubPage}
95
+ target="_blank"
96
+ className="text-(--sky-11) hover:underline font-medium"
132
97
  >
133
- <a href={molabLink} target="_blank" rel="noopener noreferrer">
134
- <MarimoPlusIcon
135
- size={12}
136
- strokeWidth={1.5}
137
- className="mr-1.5 mt-px text-(--grass-11)"
138
- />
139
- Open in molab
140
- </a>
141
- </Button>
142
- <p className="text-sm text-(--sky-12)">
143
- Run this notebook in{" "}
144
- <span className="font-semibold">molab</span>, marimo's
145
- cloud-hosted notebook platform.
146
- </p>
147
- </div>
148
- )}
98
+ marimo
99
+ </a>{" "}
100
+ notebook. {runsFromUrl ? "Run it" : "Download it, then run it"}{" "}
101
+ locally for full interactivity.
102
+ </p>
103
+
104
+ <Tabs defaultValue="uv">
105
+ <TabsList aria-label="Package manager">
106
+ <TabsTrigger value="uv">uv</TabsTrigger>
107
+ <TabsTrigger value="pip">pip</TabsTrigger>
108
+ <TabsTrigger value="conda">conda</TabsTrigger>
109
+ </TabsList>
110
+ <TabsContent value="uv">
111
+ <CommandBlock
112
+ command={`uvx marimo edit --sandbox ${quotedRunTarget}`}
113
+ />
114
+ </TabsContent>
115
+ <TabsContent value="pip">
116
+ <CommandBlock
117
+ command={`pip install marimo\nmarimo edit ${quotedRunTarget}`}
118
+ />
119
+ </TabsContent>
120
+ <TabsContent value="conda">
121
+ <CommandBlock
122
+ command={`conda install -c conda-forge marimo\nmarimo edit ${quotedRunTarget}`}
123
+ />
124
+ </TabsContent>
125
+ </Tabs>
126
+
127
+ {molabEnabled && <MolabCallout code={code} />}
128
+ </div>
149
129
  </DialogDescription>
150
130
  </DialogHeader>
151
131
  <div className="flex gap-3 pt-2">
@@ -177,3 +157,76 @@ const StaticBannerDialog = ({ code }: { code: string }) => {
177
157
  </Dialog>
178
158
  );
179
159
  };
160
+
161
+ /** Keep this in sync with StaticNotebookReader in marimo/_cli/files/file_path.py. */
162
+ export function getStaticNotebookRunTarget(
163
+ href: string,
164
+ filename: string,
165
+ ): string {
166
+ const url = new URL(href);
167
+ const isHttp = url.protocol === "http:" || url.protocol === "https:";
168
+ if (!isHttp) {
169
+ return filename;
170
+ }
171
+
172
+ const isHtmlFile = url.pathname.endsWith(".html") && url.search === "";
173
+ const matchesSupportedUrlPattern =
174
+ url.protocol === "https:" &&
175
+ (url.hostname === "marimo.app" ||
176
+ url.hostname === "links.marimo.app" ||
177
+ (url.hostname === "static.marimo.app" &&
178
+ url.pathname.startsWith("/static")) ||
179
+ url.pathname.includes("/notebooks/nb"));
180
+ if (!isHtmlFile && !matchesSupportedUrlPattern) {
181
+ return filename;
182
+ }
183
+
184
+ url.hash = "";
185
+ return url.href;
186
+ }
187
+
188
+ const CommandBlock = ({ command }: { command: string }) => (
189
+ <div className="relative rounded-lg border bg-(--sky-2) border-(--sky-7)">
190
+ <pre className="p-3 pr-10 font-mono text-(--sky-11) leading-relaxed whitespace-pre-wrap break-all">
191
+ {command}
192
+ </pre>
193
+ <Button
194
+ aria-label="Copy command"
195
+ className="absolute right-2 top-2"
196
+ variant="ghost"
197
+ size="icon"
198
+ onClick={async () => {
199
+ await copyToClipboard(command);
200
+ toast({ title: "Command copied to clipboard" });
201
+ }}
202
+ >
203
+ <CopyIcon className="w-3.5 h-3.5" />
204
+ </Button>
205
+ </div>
206
+ );
207
+
208
+ const MolabCallout = ({ code }: { code: string }) => {
209
+ const molabLink = createShareableLink({
210
+ code,
211
+ baseUrl: `${Constants.molab}/new`,
212
+ });
213
+
214
+ return (
215
+ <div className="pt-3 border-t flex gap-2 items-center">
216
+ <Button asChild={true} variant="outline" size="xs" className="shrink-0">
217
+ <a href={molabLink} target="_blank" rel="noopener noreferrer">
218
+ <MarimoPlusIcon
219
+ size={12}
220
+ strokeWidth={1.5}
221
+ className="mr-1.5 mt-px text-(--grass-11)"
222
+ />
223
+ Open in molab
224
+ </a>
225
+ </Button>
226
+ <p className="text-sm text-(--sky-12)">
227
+ Run this notebook in <span className="font-semibold">molab</span>,
228
+ marimo's cloud-hosted notebook platform.
229
+ </p>
230
+ </div>
231
+ );
232
+ };
@@ -367,6 +367,39 @@ describe("collapseConsoleOutputs", () => {
367
367
  expect(consoleOutputs[0].data).toBe("Processing: |██████████| 100/100");
368
368
  });
369
369
 
370
+ it("should preserve nested progress lines across flushed writes", () => {
371
+ let consoleOutputs: OutputMessage[] = [
372
+ {
373
+ mimetype: "text/plain",
374
+ channel: "stderr",
375
+ data: "outer: 0%",
376
+ timestamp: 0,
377
+ },
378
+ ];
379
+ const append = (data: string) => {
380
+ consoleOutputs = collapseConsoleOutputs([
381
+ ...consoleOutputs,
382
+ {
383
+ mimetype: "text/plain",
384
+ channel: "stderr",
385
+ data,
386
+ timestamp: 0,
387
+ },
388
+ ]);
389
+ };
390
+
391
+ // tqdm flushes each of these writes independently.
392
+ append("\n");
393
+ append("\rinner: 0%");
394
+ append("\u001B[A");
395
+ expect(consoleOutputs[0].data).toBe("outer: 0%\ninner: 0%");
396
+
397
+ append("\n");
398
+ append("\rinner: 1%");
399
+ append("\u001B[A");
400
+ expect(consoleOutputs[0].data).toBe("outer: 0%\ninner: 1%");
401
+ });
402
+
370
403
  it("should handle cursor up/down movements", () => {
371
404
  // Test that cursor movements within a single message are handled
372
405
  const consoleOutputs: OutputMessage[] = [
@@ -379,8 +412,8 @@ describe("collapseConsoleOutputs", () => {
379
412
  ];
380
413
 
381
414
  const result = collapseConsoleOutputs(consoleOutputs);
382
- // After moving up 1 line and writing "Modified", Line 2 gets overwritten
383
- expect(result[0].data).toBe("Line 1\nModified");
415
+ // Moving the cursor does not erase the lines below it.
416
+ expect(result[0].data).toBe("Line 1\nModified\n");
384
417
  });
385
418
 
386
419
  it("should handle clear screen sequence", () => {