@marimo-team/islands 0.23.17-dev1 → 0.23.17-dev13
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/{chat-ui-DsZJj75A.js → chat-ui-DH3hwWoI.js} +2 -2
- package/dist/{common-BGCQJb-W.js → common-D8DGPf0N.js} +5 -5
- package/dist/{html-to-image-CZ1kLKkq.js → html-to-image-CxlSazqu.js} +2095 -2088
- package/dist/main.js +5 -5
- package/dist/{process-output-bVfbcx5_.js → process-output-B24cDGzV.js} +1 -1
- package/dist/{reveal-component-NNi374so.js → reveal-component-UfXDaVe-.js} +2 -2
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/__mocks__/requests.ts +5 -0
- package/src/components/databases/icons/huggingface.svg +8 -0
- package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +1 -1
- package/src/components/editor/actions/export-dialog/__tests__/export-command.test.ts +115 -0
- package/src/components/editor/actions/export-dialog/__tests__/export-dialog.test.tsx +458 -0
- package/src/components/editor/actions/export-dialog/__tests__/export-notebook.test.ts +178 -0
- package/src/components/editor/actions/export-dialog/__tests__/state.test.ts +295 -0
- package/src/components/editor/actions/export-dialog/__tests__/use-export-dialog.test.tsx +287 -0
- package/src/components/editor/actions/export-dialog/export-command.ts +149 -0
- package/src/components/editor/actions/export-dialog/export-dialog.tsx +194 -0
- package/src/components/editor/actions/export-dialog/export-notebook.ts +98 -0
- package/src/components/editor/actions/export-dialog/format-notice.tsx +166 -0
- package/src/components/editor/actions/export-dialog/format-options.tsx +531 -0
- package/src/components/editor/actions/export-dialog/state.ts +339 -0
- package/src/components/editor/actions/export-dialog/use-export-dialog.ts +372 -0
- package/src/components/editor/actions/pair-with-agent-commands.ts +1 -21
- package/src/components/editor/actions/useNotebookActions.tsx +72 -170
- package/src/components/editor/chrome/panels/outline/__tests__/useActiveOutline.test.ts +26 -0
- package/src/components/editor/connections/add-connection-dialog.tsx +1 -1
- package/src/components/editor/connections/quick-add-data-sources.tsx +14 -7
- package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +14 -0
- package/src/components/editor/connections/storage/__tests__/as-code.test.ts +20 -0
- package/src/components/editor/connections/storage/add-storage-form.tsx +10 -0
- package/src/components/editor/connections/storage/as-code.ts +19 -1
- package/src/components/editor/connections/storage/schemas.ts +19 -0
- package/src/components/editor/controls/__tests__/notebook-menu-dropdown.test.tsx +187 -0
- package/src/components/editor/controls/notebook-menu-dropdown.tsx +4 -2
- package/src/components/storage/__tests__/storage-snippets.test.ts +88 -0
- package/src/components/storage/components.tsx +2 -0
- package/src/components/storage/storage-snippets.ts +58 -0
- package/src/core/__tests__/mode.test.ts +85 -0
- package/src/core/dom/outline.ts +25 -2
- package/src/core/mode.ts +11 -9
- package/src/core/network/__tests__/requests-lazy.test.ts +1 -0
- package/src/core/storage/types.ts +1 -0
- package/src/utils/__tests__/download.test.tsx +6 -4
- package/src/utils/download.ts +3 -1
- package/src/utils/shell.ts +17 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { Filenames } from "@/utils/filenames";
|
|
4
|
+
import { shellQuote } from "@/utils/shell";
|
|
5
|
+
import type { ExportFormat, ExportOptions, MarkdownFlavor } from "./state";
|
|
6
|
+
|
|
7
|
+
const MARKDOWN_EXTENSIONS: Record<MarkdownFlavor, string> = {
|
|
8
|
+
pymdown: "md",
|
|
9
|
+
qmd: "qmd",
|
|
10
|
+
mystmd: "myst.md",
|
|
11
|
+
mdx: "mdx",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const MARKDOWN_SUFFIXES = [".myst.md", ".markdown", ".qmd", ".mdx", ".md"];
|
|
15
|
+
|
|
16
|
+
function inferMarkdownFlavor(filename: string): MarkdownFlavor {
|
|
17
|
+
if (filename.endsWith(".myst.md")) {
|
|
18
|
+
return "mystmd";
|
|
19
|
+
}
|
|
20
|
+
if (filename.endsWith(".qmd")) {
|
|
21
|
+
return "qmd";
|
|
22
|
+
}
|
|
23
|
+
if (filename.endsWith(".mdx")) {
|
|
24
|
+
return "mdx";
|
|
25
|
+
}
|
|
26
|
+
return "pymdown";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function markdownOutputFilename(
|
|
30
|
+
filename: string,
|
|
31
|
+
selectedFlavor: MarkdownFlavor | null,
|
|
32
|
+
): string {
|
|
33
|
+
const flavor = selectedFlavor ?? inferMarkdownFlavor(filename);
|
|
34
|
+
const extension = MARKDOWN_EXTENSIONS[flavor];
|
|
35
|
+
const suffix = MARKDOWN_SUFFIXES.find((candidate) =>
|
|
36
|
+
filename.endsWith(candidate),
|
|
37
|
+
);
|
|
38
|
+
let stem = Filenames.withoutExtension(filename);
|
|
39
|
+
if (suffix) {
|
|
40
|
+
stem = filename.slice(0, -suffix.length);
|
|
41
|
+
}
|
|
42
|
+
const output = `${stem}.${extension}`;
|
|
43
|
+
if (output !== filename) {
|
|
44
|
+
return output;
|
|
45
|
+
}
|
|
46
|
+
return `${filename.slice(0, -(extension.length + 1))}.export.${extension}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type FlagValue = boolean | string | null;
|
|
50
|
+
|
|
51
|
+
function translateFlags(values: Record<string, FlagValue>): string[] {
|
|
52
|
+
return Object.entries(values).flatMap(([name, value]) => {
|
|
53
|
+
if (value === null) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
if (value === true) {
|
|
57
|
+
return [`--${name}`];
|
|
58
|
+
}
|
|
59
|
+
if (value === false) {
|
|
60
|
+
return [`--no-${name}`];
|
|
61
|
+
}
|
|
62
|
+
return [`--${name}=${value}`];
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function webPDFFlagValue(pdf: ExportOptions["pdf"]): boolean | null {
|
|
67
|
+
if (pdf.preset === "document") {
|
|
68
|
+
return pdf.webpdf;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type CommandFormat = Exclude<ExportFormat, "png">;
|
|
74
|
+
|
|
75
|
+
interface ExportCommandDefinition {
|
|
76
|
+
subcommand: string;
|
|
77
|
+
flags: (options: ExportOptions) => Record<string, FlagValue>;
|
|
78
|
+
outputFilename: (source: string, options: ExportOptions) => string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const EXPORT_COMMAND_DEFINITIONS: Record<
|
|
82
|
+
CommandFormat,
|
|
83
|
+
ExportCommandDefinition
|
|
84
|
+
> = {
|
|
85
|
+
html: {
|
|
86
|
+
subcommand: "html",
|
|
87
|
+
flags: ({ html }) => ({ "include-code": html.includeCode }),
|
|
88
|
+
outputFilename: (source) => Filenames.toHTML(source),
|
|
89
|
+
},
|
|
90
|
+
markdown: {
|
|
91
|
+
subcommand: "md",
|
|
92
|
+
flags: ({ markdown }) => ({ flavor: markdown.flavor }),
|
|
93
|
+
outputFilename: (source, { markdown }) =>
|
|
94
|
+
markdownOutputFilename(source, markdown.flavor),
|
|
95
|
+
},
|
|
96
|
+
ipynb: {
|
|
97
|
+
subcommand: "ipynb",
|
|
98
|
+
flags: ({ ipynb }) => ({
|
|
99
|
+
sort: ipynb.sortMode,
|
|
100
|
+
"include-outputs": ipynb.includeOutputs,
|
|
101
|
+
}),
|
|
102
|
+
outputFilename: (source) => Filenames.toIPYNB(source),
|
|
103
|
+
},
|
|
104
|
+
pdf: {
|
|
105
|
+
subcommand: "pdf",
|
|
106
|
+
flags: ({ pdf }) => ({
|
|
107
|
+
as: pdf.preset,
|
|
108
|
+
"include-inputs": pdf.includeInputs,
|
|
109
|
+
"include-outputs": pdf.includeOutputs,
|
|
110
|
+
webpdf: webPDFFlagValue(pdf),
|
|
111
|
+
}),
|
|
112
|
+
outputFilename: (source) => Filenames.toPDF(source),
|
|
113
|
+
},
|
|
114
|
+
script: {
|
|
115
|
+
subcommand: "script",
|
|
116
|
+
flags: () => ({}),
|
|
117
|
+
outputFilename: (source) =>
|
|
118
|
+
`${Filenames.withoutExtension(source)}.script.py`,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export function getExportCommand({
|
|
123
|
+
format,
|
|
124
|
+
filename,
|
|
125
|
+
options,
|
|
126
|
+
}: {
|
|
127
|
+
format: ExportFormat;
|
|
128
|
+
filename: string | null;
|
|
129
|
+
options: ExportOptions;
|
|
130
|
+
}): string | null {
|
|
131
|
+
if (
|
|
132
|
+
format === "png" ||
|
|
133
|
+
(format === "script" && options.script.type === "source") ||
|
|
134
|
+
!filename
|
|
135
|
+
) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const definition = EXPORT_COMMAND_DEFINITIONS[format];
|
|
140
|
+
return [
|
|
141
|
+
"marimo",
|
|
142
|
+
"export",
|
|
143
|
+
definition.subcommand,
|
|
144
|
+
shellQuote(filename),
|
|
145
|
+
...translateFlags(definition.flags(options)),
|
|
146
|
+
"-o",
|
|
147
|
+
shellQuote(definition.outputFilename(filename, options)),
|
|
148
|
+
].join(" ");
|
|
149
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { useMediaQuery } from "@uidotdev/usehooks";
|
|
4
|
+
import type React from "react";
|
|
5
|
+
import { CopyClipboardIcon } from "@/components/icons/copy-icon";
|
|
6
|
+
import { Spinner } from "@/components/icons/spinner";
|
|
7
|
+
import { Button, buttonVariants } from "@/components/ui/button";
|
|
8
|
+
import {
|
|
9
|
+
DialogContent,
|
|
10
|
+
DialogDescription,
|
|
11
|
+
DialogHeader,
|
|
12
|
+
DialogTitle,
|
|
13
|
+
} from "@/components/ui/dialog";
|
|
14
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
15
|
+
import { cn } from "@/utils/cn";
|
|
16
|
+
import { FORMAT_DEFINITIONS } from "./format-options";
|
|
17
|
+
import { FormatNotice, FormatStatusIcon } from "./format-notice";
|
|
18
|
+
import type { ExportFormat } from "./state";
|
|
19
|
+
import { useExportDialog } from "./use-export-dialog";
|
|
20
|
+
|
|
21
|
+
const DESKTOP_LAYOUT_QUERY = "(min-width: 640px)";
|
|
22
|
+
|
|
23
|
+
export const ExportDialog: React.FC<{
|
|
24
|
+
initialFormat?: ExportFormat;
|
|
25
|
+
onClose: () => void;
|
|
26
|
+
returnFocusRef?: React.RefObject<HTMLElement | null>;
|
|
27
|
+
}> = ({ initialFormat, onClose, returnFocusRef }) => {
|
|
28
|
+
const {
|
|
29
|
+
dialogRef,
|
|
30
|
+
isExporting,
|
|
31
|
+
formats,
|
|
32
|
+
options,
|
|
33
|
+
selected,
|
|
34
|
+
selectFormat,
|
|
35
|
+
updateOptions,
|
|
36
|
+
submit,
|
|
37
|
+
} = useExportDialog({ initialFormat, onClose });
|
|
38
|
+
const desktopLayout = useMediaQuery(DESKTOP_LAYOUT_QUERY);
|
|
39
|
+
const {
|
|
40
|
+
format,
|
|
41
|
+
status,
|
|
42
|
+
usesBrowserPrint,
|
|
43
|
+
actionLabel,
|
|
44
|
+
command,
|
|
45
|
+
footerDescription,
|
|
46
|
+
} = selected;
|
|
47
|
+
const definition = FORMAT_DEFINITIONS[format];
|
|
48
|
+
const FormatOptions = definition.Options;
|
|
49
|
+
|
|
50
|
+
const returnFocusProps = returnFocusRef
|
|
51
|
+
? {
|
|
52
|
+
onCloseAutoFocus: (event: Event) => {
|
|
53
|
+
event.preventDefault();
|
|
54
|
+
returnFocusRef.current?.focus();
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
: {};
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<DialogContent
|
|
61
|
+
ref={dialogRef}
|
|
62
|
+
className="grid h-dvh max-h-[760px] min-w-0 w-[calc(100vw-2rem)] grid-rows-[auto_minmax(0,1fr)_auto] gap-0 overflow-hidden p-0 sm:top-[5vh] sm:h-[90vh] sm:max-h-[640px] sm:max-w-3xl"
|
|
63
|
+
data-testid="export-dialog"
|
|
64
|
+
{...returnFocusProps}
|
|
65
|
+
>
|
|
66
|
+
<DialogHeader className="border-b px-5 py-4 pr-12">
|
|
67
|
+
<DialogTitle>Export notebook</DialogTitle>
|
|
68
|
+
<DialogDescription>
|
|
69
|
+
Choose a format and adjust its options.
|
|
70
|
+
</DialogDescription>
|
|
71
|
+
</DialogHeader>
|
|
72
|
+
|
|
73
|
+
<Tabs
|
|
74
|
+
value={format}
|
|
75
|
+
onValueChange={selectFormat}
|
|
76
|
+
orientation={desktopLayout ? "vertical" : "horizontal"}
|
|
77
|
+
className="flex min-h-0 min-w-0 flex-col overflow-hidden sm:grid sm:grid-cols-[168px_minmax(0,1fr)]"
|
|
78
|
+
>
|
|
79
|
+
<TabsList
|
|
80
|
+
aria-label="Export format"
|
|
81
|
+
className="grid max-h-none shrink-0 grid-cols-2 items-stretch justify-start gap-1 overflow-auto rounded-none border-b bg-muted/20 p-2 min-[360px]:grid-cols-3 sm:flex sm:h-full sm:flex-col sm:border-b-0 sm:border-r"
|
|
82
|
+
>
|
|
83
|
+
{formats.map(({ format: candidate, status: candidateStatus }) => {
|
|
84
|
+
const candidateDefinition = FORMAT_DEFINITIONS[candidate];
|
|
85
|
+
return (
|
|
86
|
+
<TabsTrigger
|
|
87
|
+
key={candidate}
|
|
88
|
+
value={candidate}
|
|
89
|
+
disabled={isExporting}
|
|
90
|
+
className="min-w-0 justify-start gap-2 px-2.5 py-2 text-xs data-[state=active]:shadow-xs sm:w-full sm:text-sm"
|
|
91
|
+
data-testid={`export-format-${candidate}`}
|
|
92
|
+
>
|
|
93
|
+
<candidateDefinition.Icon
|
|
94
|
+
className="size-3.5 shrink-0"
|
|
95
|
+
strokeWidth={1.5}
|
|
96
|
+
/>
|
|
97
|
+
<span className="truncate">{candidateDefinition.label}</span>
|
|
98
|
+
<FormatStatusIcon status={candidateStatus} />
|
|
99
|
+
</TabsTrigger>
|
|
100
|
+
);
|
|
101
|
+
})}
|
|
102
|
+
</TabsList>
|
|
103
|
+
|
|
104
|
+
{/* One panel per tab so each trigger's aria-controls resolves. */}
|
|
105
|
+
{formats.map(({ format: candidate }) => (
|
|
106
|
+
<TabsContent
|
|
107
|
+
key={candidate}
|
|
108
|
+
value={candidate}
|
|
109
|
+
className="mt-0 min-h-0 min-w-0 overflow-y-auto px-4 py-3"
|
|
110
|
+
>
|
|
111
|
+
<div className="mb-3">
|
|
112
|
+
<h3 className="text-base font-semibold">{definition.label}</h3>
|
|
113
|
+
<p className="mt-0.5 text-sm text-muted-foreground">
|
|
114
|
+
{definition.description}
|
|
115
|
+
</p>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<FormatNotice
|
|
119
|
+
format={format}
|
|
120
|
+
formatLabel={definition.label}
|
|
121
|
+
status={status}
|
|
122
|
+
/>
|
|
123
|
+
|
|
124
|
+
{usesBrowserPrint ? null : (
|
|
125
|
+
<FormatOptions
|
|
126
|
+
options={options}
|
|
127
|
+
updateOptions={updateOptions}
|
|
128
|
+
disabled={isExporting}
|
|
129
|
+
/>
|
|
130
|
+
)}
|
|
131
|
+
</TabsContent>
|
|
132
|
+
))}
|
|
133
|
+
</Tabs>
|
|
134
|
+
|
|
135
|
+
<footer className="min-w-0 border-t bg-muted/20 px-4 py-3">
|
|
136
|
+
{command ? (
|
|
137
|
+
<div className="flex min-w-0 items-center gap-2 rounded-md border bg-background px-2.5 py-2 font-mono text-xs">
|
|
138
|
+
<span
|
|
139
|
+
className="select-none text-muted-foreground"
|
|
140
|
+
aria-hidden={true}
|
|
141
|
+
>
|
|
142
|
+
$
|
|
143
|
+
</span>
|
|
144
|
+
<code
|
|
145
|
+
className="min-w-0 flex-1 overflow-x-auto whitespace-nowrap"
|
|
146
|
+
data-testid="export-cli-command"
|
|
147
|
+
aria-label="Equivalent POSIX shell command"
|
|
148
|
+
>
|
|
149
|
+
{command}
|
|
150
|
+
</code>
|
|
151
|
+
<CopyClipboardIcon
|
|
152
|
+
value={command}
|
|
153
|
+
className="size-3.5"
|
|
154
|
+
buttonClassName={cn(
|
|
155
|
+
buttonVariants({ variant: "ghost", size: "icon" }),
|
|
156
|
+
"shrink-0",
|
|
157
|
+
)}
|
|
158
|
+
tooltip="Copy POSIX shell command"
|
|
159
|
+
ariaLabel="Copy POSIX shell command"
|
|
160
|
+
toastTitle="Command copied"
|
|
161
|
+
/>
|
|
162
|
+
</div>
|
|
163
|
+
) : null}
|
|
164
|
+
|
|
165
|
+
<div
|
|
166
|
+
className={cn(
|
|
167
|
+
"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between",
|
|
168
|
+
command && "mt-3",
|
|
169
|
+
)}
|
|
170
|
+
>
|
|
171
|
+
{footerDescription ? (
|
|
172
|
+
<p className="text-xs text-muted-foreground">{footerDescription}</p>
|
|
173
|
+
) : null}
|
|
174
|
+
<Button
|
|
175
|
+
type="button"
|
|
176
|
+
disabled={!status.available || isExporting}
|
|
177
|
+
aria-busy={isExporting}
|
|
178
|
+
onClick={submit}
|
|
179
|
+
className={cn(
|
|
180
|
+
"border-(--blue-11) bg-(--blue-11) hover:border-(--blue-12) hover:bg-(--blue-12) sm:min-w-36 dark:border-(--blue-7) dark:bg-(--blue-5) dark:text-(--blue-12) dark:hover:border-(--blue-8) dark:hover:bg-(--blue-6)",
|
|
181
|
+
!footerDescription && "sm:ml-auto",
|
|
182
|
+
)}
|
|
183
|
+
data-testid="export-submit"
|
|
184
|
+
>
|
|
185
|
+
{isExporting && (
|
|
186
|
+
<Spinner size="small" className="mr-2" aria-hidden={true} />
|
|
187
|
+
)}
|
|
188
|
+
{usesBrowserPrint ? "Print to PDF" : actionLabel}
|
|
189
|
+
</Button>
|
|
190
|
+
</div>
|
|
191
|
+
</footer>
|
|
192
|
+
</DialogContent>
|
|
193
|
+
);
|
|
194
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import type { EditRequests, ExportedFile } from "@/core/network/types";
|
|
4
|
+
import { assertNever } from "@/utils/assertNever";
|
|
5
|
+
import { runServerSidePDFDownload } from "../pdf-export";
|
|
6
|
+
import type { ExportFormat, ExportOptions } from "./state";
|
|
7
|
+
|
|
8
|
+
type ExportRequests = Pick<
|
|
9
|
+
EditRequests,
|
|
10
|
+
| "exportAsHTML"
|
|
11
|
+
| "exportAsMarkdown"
|
|
12
|
+
| "exportAsIPYNB"
|
|
13
|
+
| "exportAsPDF"
|
|
14
|
+
| "exportAsScript"
|
|
15
|
+
| "readCode"
|
|
16
|
+
>;
|
|
17
|
+
|
|
18
|
+
export async function exportNotebook({
|
|
19
|
+
format,
|
|
20
|
+
options,
|
|
21
|
+
requests,
|
|
22
|
+
sourceFilename,
|
|
23
|
+
htmlFiles,
|
|
24
|
+
captureOutputs,
|
|
25
|
+
capturePNG,
|
|
26
|
+
downloadFile,
|
|
27
|
+
}: {
|
|
28
|
+
format: ExportFormat;
|
|
29
|
+
options: ExportOptions;
|
|
30
|
+
requests: ExportRequests;
|
|
31
|
+
sourceFilename: string;
|
|
32
|
+
htmlFiles: string[];
|
|
33
|
+
captureOutputs: () => Promise<void>;
|
|
34
|
+
capturePNG: () => Promise<void>;
|
|
35
|
+
downloadFile: (file: ExportedFile) => void;
|
|
36
|
+
}): Promise<void> {
|
|
37
|
+
switch (format) {
|
|
38
|
+
case "html": {
|
|
39
|
+
const file = await requests.exportAsHTML({
|
|
40
|
+
download: false,
|
|
41
|
+
files: htmlFiles,
|
|
42
|
+
includeCode: options.html.includeCode,
|
|
43
|
+
});
|
|
44
|
+
downloadFile(file);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
case "markdown": {
|
|
48
|
+
const file = await requests.exportAsMarkdown({
|
|
49
|
+
download: false,
|
|
50
|
+
flavor: options.markdown.flavor,
|
|
51
|
+
});
|
|
52
|
+
downloadFile(file);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
case "ipynb": {
|
|
56
|
+
if (options.ipynb.includeOutputs) {
|
|
57
|
+
await captureOutputs();
|
|
58
|
+
}
|
|
59
|
+
const file = await requests.exportAsIPYNB({
|
|
60
|
+
download: false,
|
|
61
|
+
sortMode: options.ipynb.sortMode,
|
|
62
|
+
includeOutputs: options.ipynb.includeOutputs,
|
|
63
|
+
});
|
|
64
|
+
downloadFile(file);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
case "pdf":
|
|
68
|
+
await runServerSidePDFDownload({
|
|
69
|
+
exportOptions: options.pdf,
|
|
70
|
+
captureOutputs,
|
|
71
|
+
downloadPDF: async (exportOptions) => {
|
|
72
|
+
const file = await requests.exportAsPDF(exportOptions);
|
|
73
|
+
downloadFile(file);
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
|
|
78
|
+
case "script": {
|
|
79
|
+
if (options.script.type === "source") {
|
|
80
|
+
const source = await requests.readCode();
|
|
81
|
+
downloadFile({
|
|
82
|
+
contents: source.contents,
|
|
83
|
+
filename: sourceFilename,
|
|
84
|
+
mediaType: "text/plain",
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const file = await requests.exportAsScript({ download: false });
|
|
89
|
+
downloadFile(file);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
case "png":
|
|
93
|
+
await capturePNG();
|
|
94
|
+
return;
|
|
95
|
+
default:
|
|
96
|
+
assertNever(format);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { AlertCircleIcon } from "lucide-react";
|
|
4
|
+
import type { PropsWithChildren } from "react";
|
|
5
|
+
import { Spinner } from "@/components/icons/spinner";
|
|
6
|
+
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
7
|
+
import { assertNever } from "@/utils/assertNever";
|
|
8
|
+
import { cn } from "@/utils/cn";
|
|
9
|
+
import type {
|
|
10
|
+
ExportBlockReason,
|
|
11
|
+
ExportFormat,
|
|
12
|
+
ExportFormatStatus,
|
|
13
|
+
ExportSetupRequirement,
|
|
14
|
+
} from "./state";
|
|
15
|
+
|
|
16
|
+
const SETUP_REQUIREMENTS: Record<
|
|
17
|
+
ExportSetupRequirement["name"],
|
|
18
|
+
{ description: string }
|
|
19
|
+
> = {
|
|
20
|
+
"playwright-chromium": {
|
|
21
|
+
description: "PDF export requires Playwright Chromium.",
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function FormatStatusIcon({ status }: { status: ExportFormatStatus }) {
|
|
26
|
+
if (status.available && !status.availabilityCheckFailed) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
if (status.reason?.type === "checking-requirements") {
|
|
30
|
+
return (
|
|
31
|
+
<span className="ml-auto flex shrink-0">
|
|
32
|
+
<Spinner size="small" className="size-3" />
|
|
33
|
+
<span className="sr-only">Checking requirements</span>
|
|
34
|
+
</span>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
const statusLabel = status.availabilityCheckFailed
|
|
38
|
+
? "Requirements unknown"
|
|
39
|
+
: "Unavailable";
|
|
40
|
+
return (
|
|
41
|
+
<span className="ml-auto flex shrink-0">
|
|
42
|
+
<AlertCircleIcon
|
|
43
|
+
className={cn(
|
|
44
|
+
"size-3",
|
|
45
|
+
status.availabilityCheckFailed
|
|
46
|
+
? "text-muted-foreground"
|
|
47
|
+
: "text-(--yellow-11)",
|
|
48
|
+
)}
|
|
49
|
+
/>
|
|
50
|
+
<span className="sr-only">{statusLabel}</span>
|
|
51
|
+
</span>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function FormatNotice({
|
|
56
|
+
format,
|
|
57
|
+
formatLabel,
|
|
58
|
+
status,
|
|
59
|
+
}: {
|
|
60
|
+
format: ExportFormat;
|
|
61
|
+
formatLabel: string;
|
|
62
|
+
status: ExportFormatStatus;
|
|
63
|
+
}) {
|
|
64
|
+
if (status.availabilityCheckFailed) {
|
|
65
|
+
return (
|
|
66
|
+
<div className="mb-2.5">
|
|
67
|
+
<Notice>
|
|
68
|
+
Couldn't check whether this export is available. You can still try it.
|
|
69
|
+
</Notice>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
if (!status.reason) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className="mb-2.5">
|
|
79
|
+
<ReasonNotice
|
|
80
|
+
format={format}
|
|
81
|
+
formatLabel={formatLabel}
|
|
82
|
+
reason={status.reason}
|
|
83
|
+
/>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function Notice({ children }: PropsWithChildren) {
|
|
89
|
+
return (
|
|
90
|
+
<Alert
|
|
91
|
+
variant="warning"
|
|
92
|
+
className="flex items-start gap-2.5 p-3 text-(--yellow-12) has-[svg]:pl-3 sm:items-center [&>svg]:static [&>svg+div]:translate-y-0"
|
|
93
|
+
>
|
|
94
|
+
<AlertCircleIcon className="mt-0.5 size-4 shrink-0 sm:mt-0" />
|
|
95
|
+
<AlertDescription className="min-w-0 flex-1 text-sm leading-5">
|
|
96
|
+
{children}
|
|
97
|
+
</AlertDescription>
|
|
98
|
+
</Alert>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function ReasonNotice({
|
|
103
|
+
format,
|
|
104
|
+
formatLabel,
|
|
105
|
+
reason,
|
|
106
|
+
}: {
|
|
107
|
+
format: ExportFormat;
|
|
108
|
+
formatLabel: string;
|
|
109
|
+
reason: ExportBlockReason;
|
|
110
|
+
}) {
|
|
111
|
+
switch (reason.type) {
|
|
112
|
+
case "checking-requirements":
|
|
113
|
+
return (
|
|
114
|
+
<Alert
|
|
115
|
+
role="status"
|
|
116
|
+
variant="info"
|
|
117
|
+
className="flex items-center gap-2.5 p-3 has-[svg]:pl-3 [&>svg]:static [&>svg+div]:translate-y-0"
|
|
118
|
+
>
|
|
119
|
+
<Spinner className="size-4 shrink-0" size="small" />
|
|
120
|
+
<AlertDescription className="min-w-0 flex-1 text-sm leading-5">
|
|
121
|
+
Checking {formatLabel} requirements…
|
|
122
|
+
</AlertDescription>
|
|
123
|
+
</Alert>
|
|
124
|
+
);
|
|
125
|
+
case "notebook-must-be-named":
|
|
126
|
+
return <Notice>Name and save this notebook before exporting.</Notice>;
|
|
127
|
+
case "missing-packages":
|
|
128
|
+
return (
|
|
129
|
+
<Notice>
|
|
130
|
+
Install{" "}
|
|
131
|
+
<code className="break-words font-mono">
|
|
132
|
+
{reason.packages.join(", ")}
|
|
133
|
+
</code>{" "}
|
|
134
|
+
where marimo is running to use this export.
|
|
135
|
+
</Notice>
|
|
136
|
+
);
|
|
137
|
+
case "missing-setup":
|
|
138
|
+
return (
|
|
139
|
+
<Notice>
|
|
140
|
+
<span className="block min-w-0">
|
|
141
|
+
{reason.requirements.map((requirement) => {
|
|
142
|
+
const details = SETUP_REQUIREMENTS[requirement.name];
|
|
143
|
+
return (
|
|
144
|
+
<span className="block" key={requirement.name}>
|
|
145
|
+
{details.description}
|
|
146
|
+
<code className="mt-1 block break-all font-mono">
|
|
147
|
+
{requirement.command}
|
|
148
|
+
</code>
|
|
149
|
+
</span>
|
|
150
|
+
);
|
|
151
|
+
})}
|
|
152
|
+
</span>
|
|
153
|
+
</Notice>
|
|
154
|
+
);
|
|
155
|
+
case "wasm-runtime":
|
|
156
|
+
return (
|
|
157
|
+
<Notice>
|
|
158
|
+
{format === "pdf"
|
|
159
|
+
? "Use your browser's print dialog to save the current app view as a PDF."
|
|
160
|
+
: "Open this notebook in a local marimo session to use this export."}
|
|
161
|
+
</Notice>
|
|
162
|
+
);
|
|
163
|
+
default:
|
|
164
|
+
assertNever(reason);
|
|
165
|
+
}
|
|
166
|
+
}
|