@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.
Files changed (46) hide show
  1. package/dist/{chat-ui-DsZJj75A.js → chat-ui-DH3hwWoI.js} +2 -2
  2. package/dist/{common-BGCQJb-W.js → common-D8DGPf0N.js} +5 -5
  3. package/dist/{html-to-image-CZ1kLKkq.js → html-to-image-CxlSazqu.js} +2095 -2088
  4. package/dist/main.js +5 -5
  5. package/dist/{process-output-bVfbcx5_.js → process-output-B24cDGzV.js} +1 -1
  6. package/dist/{reveal-component-NNi374so.js → reveal-component-UfXDaVe-.js} +2 -2
  7. package/dist/style.css +1 -1
  8. package/package.json +1 -1
  9. package/src/__mocks__/requests.ts +5 -0
  10. package/src/components/databases/icons/huggingface.svg +8 -0
  11. package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +1 -1
  12. package/src/components/editor/actions/export-dialog/__tests__/export-command.test.ts +115 -0
  13. package/src/components/editor/actions/export-dialog/__tests__/export-dialog.test.tsx +458 -0
  14. package/src/components/editor/actions/export-dialog/__tests__/export-notebook.test.ts +178 -0
  15. package/src/components/editor/actions/export-dialog/__tests__/state.test.ts +295 -0
  16. package/src/components/editor/actions/export-dialog/__tests__/use-export-dialog.test.tsx +287 -0
  17. package/src/components/editor/actions/export-dialog/export-command.ts +149 -0
  18. package/src/components/editor/actions/export-dialog/export-dialog.tsx +194 -0
  19. package/src/components/editor/actions/export-dialog/export-notebook.ts +98 -0
  20. package/src/components/editor/actions/export-dialog/format-notice.tsx +166 -0
  21. package/src/components/editor/actions/export-dialog/format-options.tsx +531 -0
  22. package/src/components/editor/actions/export-dialog/state.ts +339 -0
  23. package/src/components/editor/actions/export-dialog/use-export-dialog.ts +372 -0
  24. package/src/components/editor/actions/pair-with-agent-commands.ts +1 -21
  25. package/src/components/editor/actions/useNotebookActions.tsx +72 -170
  26. package/src/components/editor/chrome/panels/outline/__tests__/useActiveOutline.test.ts +26 -0
  27. package/src/components/editor/connections/add-connection-dialog.tsx +1 -1
  28. package/src/components/editor/connections/quick-add-data-sources.tsx +14 -7
  29. package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +14 -0
  30. package/src/components/editor/connections/storage/__tests__/as-code.test.ts +20 -0
  31. package/src/components/editor/connections/storage/add-storage-form.tsx +10 -0
  32. package/src/components/editor/connections/storage/as-code.ts +19 -1
  33. package/src/components/editor/connections/storage/schemas.ts +19 -0
  34. package/src/components/editor/controls/__tests__/notebook-menu-dropdown.test.tsx +187 -0
  35. package/src/components/editor/controls/notebook-menu-dropdown.tsx +4 -2
  36. package/src/components/storage/__tests__/storage-snippets.test.ts +88 -0
  37. package/src/components/storage/components.tsx +2 -0
  38. package/src/components/storage/storage-snippets.ts +58 -0
  39. package/src/core/__tests__/mode.test.ts +85 -0
  40. package/src/core/dom/outline.ts +25 -2
  41. package/src/core/mode.ts +11 -9
  42. package/src/core/network/__tests__/requests-lazy.test.ts +1 -0
  43. package/src/core/storage/types.ts +1 -0
  44. package/src/utils/__tests__/download.test.tsx +6 -4
  45. package/src/utils/download.ts +3 -1
  46. package/src/utils/shell.ts +17 -0
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { assertNever } from "@/utils/assertNever";
4
4
  import { KnownQueryParams } from "@/core/constants";
5
+ import { shellQuote } from "@/utils/shell";
5
6
 
6
7
  export type AgentTab = "claude" | "codex" | "opencode" | "prompt";
7
8
 
@@ -23,27 +24,6 @@ export function getMarimoCommand(): string {
23
24
  return import.meta.env.DEV ? "uv run marimo" : "uvx marimo@latest";
24
25
  }
25
26
 
26
- /**
27
- * POSIX-quote a value for safe embedding in a shell command. These commands are
28
- * meant to be copied into a terminal, so a url/token containing shell
29
- * metacharacters (`'`, `&`, `$(...)`, ...) must not break out of its argument.
30
- *
31
- * Mirrors Python's `shlex.quote` (used on the CLI side in
32
- * `marimo/_cli/pair/commands.py`) so both sides produce identical commands:
33
- * values that are already shell-safe are left as-is for readability, and
34
- * anything else is single-quoted with embedded quotes escaped as `'"'"'`.
35
- */
36
- export function shellQuote(value: string): string {
37
- if (value === "") {
38
- return "''";
39
- }
40
- // Same "safe" character set as CPython's shlex.quote (ASCII \w plus a few).
41
- if (/^[\w@%+=:,./-]+$/.test(value)) {
42
- return value;
43
- }
44
- return `'${value.replaceAll("'", `'"'"'`)}'`;
45
- }
46
-
47
27
  /** Return the server file key from a page URL, preserving its decoded value. */
48
28
  export function getFileFromURL(href: string): string | undefined {
49
29
  const file = new URL(href).searchParams.get(KnownQueryParams.filePath);
@@ -1,6 +1,7 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import { useAtom, useAtomValue, useSetAtom } from "jotai";
4
+ import type { RefObject } from "react";
4
5
  import {
5
6
  BookMarkedIcon,
6
7
  CheckIcon,
@@ -51,7 +52,6 @@ import { GitHubIcon } from "@/components/icons/github";
51
52
  import { MarimoPlusIcon } from "@/components/icons/marimo-icons";
52
53
  import { YouTubeIcon } from "@/components/icons/youtube";
53
54
  import { useImperativeModal } from "@/components/modal/ImperativeModal";
54
- import { renderShortcut } from "@/components/shortcuts/renderShortcut";
55
55
  import { PairWithAgentModal } from "@/components/editor/actions/pair-with-agent-modal";
56
56
  import { ShareStaticNotebookModal } from "@/components/static-html/share-modal";
57
57
  import { toast } from "@/components/ui/use-toast";
@@ -66,30 +66,15 @@ import { disabledCellIds } from "@/core/cells/utils";
66
66
  import { capabilitiesAtom } from "@/core/config/capabilities";
67
67
  import { aiEnabledAtom, useResolvedMarimoConfig } from "@/core/config/config";
68
68
  import { Constants } from "@/core/constants";
69
- import {
70
- updateCellOutputsWithScreenshots,
71
- useEnrichCellOutputs,
72
- } from "@/core/export/hooks";
73
69
  import { useLayoutActions, useLayoutState } from "@/core/layout/layout";
74
70
  import { useTogglePresenting } from "@/core/layout/useTogglePresenting";
75
71
  import { kioskModeAtom, viewStateAtom } from "@/core/mode";
76
72
  import { useRequestClient } from "@/core/network/requests";
77
73
  import { useFilename } from "@/core/saving/filename";
78
- import { downloadAsHTML } from "@/core/static/download-html";
79
74
  import { createShareableLink } from "@/core/wasm/share";
80
75
  import { isWasm } from "@/core/wasm/utils";
81
76
  import { copyToClipboard } from "@/utils/copy";
82
- import {
83
- ADD_PRINTING_CLASS,
84
- downloadAsPDF,
85
- downloadBlob,
86
- downloadExportedFile,
87
- downloadHTMLAsImage,
88
- withLoadingToast,
89
- } from "@/utils/download";
90
- import { Filenames } from "@/utils/filenames";
91
77
  import { Objects } from "@/utils/objects";
92
- import type { ProgressState } from "@/utils/progress";
93
78
  import { Strings } from "@/utils/strings";
94
79
  import { newNotebookURL } from "@/utils/urls";
95
80
  import { useRunAllCells } from "../cell/useRunCells";
@@ -100,7 +85,13 @@ import { keyboardShortcutsAtom } from "../controls/keyboard-shortcuts";
100
85
  import { commandPaletteAtom } from "../controls/state";
101
86
  import { displayLayoutName, getLayoutIcon } from "../renderers/layout-select";
102
87
  import { LAYOUT_TYPES } from "../renderers/types";
103
- import { runServerSidePDFDownload } from "./pdf-export";
88
+ import { ExportDialog } from "./export-dialog/export-dialog";
89
+ import {
90
+ applyExportOptionOverrides,
91
+ exportOptionsAtom,
92
+ type ExportFormat,
93
+ type ExportOptionOverrides,
94
+ } from "./export-dialog/state";
104
95
  import type { ActionButton } from "./types";
105
96
  import { useCopyNotebook } from "./useCopyNotebook";
106
97
  import { useRestartKernel } from "./useRestartKernel";
@@ -111,7 +102,11 @@ const NOOP_HANDLER = (event?: Event) => {
111
102
  event?.stopPropagation();
112
103
  };
113
104
 
114
- export function useNotebookActions() {
105
+ export function useNotebookActions({
106
+ exportDialogReturnFocusRef,
107
+ }: {
108
+ exportDialogReturnFocusRef?: RefObject<HTMLElement | null>;
109
+ } = {}) {
115
110
  const filename = useFilename();
116
111
  const { openModal, closeModal } = useImperativeModal();
117
112
  const { toggleApplication } = useChromeActions();
@@ -138,15 +133,8 @@ export function useNotebookActions() {
138
133
  const setSettingsDialogOpen = useSetAtom(settingDialogAtom);
139
134
  const { handleClick: openSettings } = useOpenSettingsToTab();
140
135
  const setKeyboardShortcutsOpen = useSetAtom(keyboardShortcutsAtom);
141
- const {
142
- exportAsIPYNB,
143
- exportAsMarkdown,
144
- exportAsScript,
145
- readCode,
146
- saveCellConfig,
147
- updateCellOutputs,
148
- } = useRequestClient();
149
- const takeScreenshots = useEnrichCellOutputs();
136
+ const setExportOptions = useSetAtom(exportOptionsAtom);
137
+ const { readCode, saveCellConfig } = useRequestClient();
150
138
 
151
139
  const hasDisabledCells = useAtomValue(hasDisabledCellsAtom);
152
140
  const canUndoDeletes = useAtomValue(canUndoDeletesAtom);
@@ -158,10 +146,6 @@ export function useNotebookActions() {
158
146
  const sharingHtmlEnabled = resolvedConfig.sharing?.html ?? true;
159
147
  const sharingWasmEnabled = resolvedConfig.sharing?.wasm ?? true;
160
148
  const sharingMolabEnabled = resolvedConfig.sharing?.molab ?? true;
161
-
162
- // Server-side PDF export is always available outside WASM.
163
- // Browser print fallback is used in WASM.
164
- const serverSidePdfEnabled = !isWasm();
165
149
  const isSlidesLayout = selectedLayout === "slides";
166
150
 
167
151
  const renderCheckboxElement = (checked: boolean) => (
@@ -170,82 +154,39 @@ export function useNotebookActions() {
170
154
  </div>
171
155
  );
172
156
 
173
- const renderRecommendedElement = (recommended: boolean) => {
174
- if (!recommended) {
175
- return null;
157
+ const openExportDialog = (
158
+ initialFormat?: ExportFormat,
159
+ optionOverrides?: ExportOptionOverrides,
160
+ ) => {
161
+ if (optionOverrides) {
162
+ setExportOptions((current) =>
163
+ applyExportOptionOverrides(current, optionOverrides),
164
+ );
176
165
  }
177
- return (
178
- <span className="ml-3 shrink-0 rounded-full border border-emerald-200 bg-emerald-50 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-emerald-700">
179
- Recommended
180
- </span>
166
+ openModal(
167
+ <ExportDialog
168
+ initialFormat={initialFormat}
169
+ onClose={closeModal}
170
+ returnFocusRef={exportDialogReturnFocusRef}
171
+ />,
181
172
  );
182
173
  };
183
174
 
184
- const downloadServerSidePDF = async ({
185
- preset,
186
- title,
187
- }: {
188
- preset: "document" | "slides";
189
- title: string;
190
- }) => {
191
- if (!filename) {
192
- toastNotebookMustBeNamed();
193
- return;
194
- }
195
-
196
- const runDownload = async (progress: ProgressState) => {
197
- await runServerSidePDFDownload({
198
- exportOptions: {
199
- webpdf: false,
200
- preset,
201
- includeInputs: true,
202
- includeOutputs: true,
203
- },
204
- captureOutputs: () =>
205
- updateCellOutputsWithScreenshots({
206
- takeScreenshots: () => takeScreenshots({ progress }),
207
- updateCellOutputs,
208
- }),
209
- downloadPDF: downloadAsPDF,
210
- });
211
- };
212
- await withLoadingToast(title, runDownload);
213
- };
214
-
215
- const handleDocumentPDF = async () => {
216
- if (serverSidePdfEnabled) {
217
- await downloadServerSidePDF({
218
- preset: "document",
219
- title: "Downloading Document PDF...",
220
- });
221
- return;
222
- }
223
- const beforeprint = new Event("export-beforeprint");
224
- const afterprint = new Event("export-afterprint");
225
- window.dispatchEvent(beforeprint);
226
- setTimeout(() => window.print(), 0);
227
- setTimeout(() => window.dispatchEvent(afterprint), 0);
228
- };
229
-
230
- const handleDownloadAsIPYNB = async () => {
231
- if (!filename) {
232
- toastNotebookMustBeNamed();
233
- return;
234
- }
235
-
236
- const runDownload = async (progress: ProgressState) => {
237
- await updateCellOutputsWithScreenshots({
238
- takeScreenshots: () => takeScreenshots({ progress }),
239
- updateCellOutputs,
240
- });
241
- const exportedFile = await exportAsIPYNB({ download: false });
242
- downloadExportedFile(exportedFile);
243
- };
244
-
245
- await withLoadingToast("Downloading IPYNB...", runDownload);
246
- };
247
-
248
175
  const actions: ActionButton[] = [
176
+ {
177
+ icon: <DownloadIcon size={14} strokeWidth={1.5} />,
178
+ label: "Export…",
179
+ additionalKeywords: [
180
+ "download",
181
+ "html",
182
+ "markdown",
183
+ "ipynb",
184
+ "pdf",
185
+ "script",
186
+ "png",
187
+ ],
188
+ handle: () => openExportDialog(),
189
+ },
249
190
  {
250
191
  icon: <DownloadIcon size={14} strokeWidth={1.5} />,
251
192
  label: "Download",
@@ -254,83 +195,44 @@ export function useNotebookActions() {
254
195
  {
255
196
  icon: <FolderDownIcon size={14} strokeWidth={1.5} />,
256
197
  label: "Download as HTML",
257
- handle: async () => {
258
- if (!filename) {
259
- toastNotebookMustBeNamed();
260
- return;
261
- }
262
- await downloadAsHTML({ includeCode: true });
263
- },
198
+ handle: () =>
199
+ openExportDialog("html", { html: { includeCode: true } }),
264
200
  },
265
201
  {
266
202
  icon: <FolderDownIcon size={14} strokeWidth={1.5} />,
267
203
  label: "Download as HTML (exclude code)",
268
- handle: async () => {
269
- if (!filename) {
270
- toastNotebookMustBeNamed();
271
- return;
272
- }
273
- await downloadAsHTML({ includeCode: false });
274
- },
204
+ handle: () =>
205
+ openExportDialog("html", { html: { includeCode: false } }),
275
206
  },
276
207
  {
277
208
  icon: (
278
209
  <MarkdownIcon strokeWidth={1.5} style={{ width: 14, height: 14 }} />
279
210
  ),
280
211
  label: "Download as Markdown",
281
- handle: async () => {
282
- const exportedFile = await exportAsMarkdown({ download: false });
283
- downloadExportedFile(exportedFile);
284
- },
212
+ handle: () => openExportDialog("markdown"),
285
213
  },
286
214
  {
287
215
  icon: <NotebookIcon size={14} strokeWidth={1.5} />,
288
216
  label: "Download as ipynb",
289
- handle: handleDownloadAsIPYNB,
217
+ handle: () => openExportDialog("ipynb"),
290
218
  },
291
219
  {
292
220
  icon: <CodeIcon size={14} strokeWidth={1.5} />,
293
221
  label: "Download notebook source",
294
- handle: async () => {
295
- const code = await readCode();
296
- downloadBlob(
297
- new Blob([code.contents], { type: "text/plain" }),
298
- Filenames.toPY(document.title),
299
- );
300
- },
222
+ handle: () =>
223
+ openExportDialog("script", { script: { type: "source" } }),
301
224
  },
302
225
  {
303
226
  icon: <CodeIcon size={14} strokeWidth={1.5} />,
304
227
  label: "Download flat script",
305
- handle: async () => {
306
- const exportedFile = await exportAsScript({ download: false });
307
- downloadExportedFile(exportedFile);
308
- },
228
+ handle: () =>
229
+ openExportDialog("script", { script: { type: "flat" } }),
309
230
  },
310
231
  {
311
232
  divider: true,
312
233
  icon: <ImageIcon size={14} strokeWidth={1.5} />,
313
234
  label: "Download as PNG",
314
- disabled: viewState.mode !== "present",
315
- tooltip:
316
- viewState.mode === "present" ? undefined : (
317
- <span>
318
- Only available in app view. <br />
319
- Toggle with: {renderShortcut("global.hideCode", false)}
320
- </span>
321
- ),
322
- handle: async () => {
323
- const app = document.getElementById("App");
324
- if (!app) {
325
- return;
326
- }
327
- await downloadHTMLAsImage({
328
- element: app,
329
- filename: document.title,
330
- // Add body.printing ONLY when converting the whole notebook to a screenshot
331
- prepare: ADD_PRINTING_CLASS,
332
- });
333
- },
235
+ handle: () => openExportDialog("png"),
334
236
  },
335
237
  isSlidesLayout
336
238
  ? {
@@ -342,19 +244,24 @@ export function useNotebookActions() {
342
244
  {
343
245
  icon: <FileIcon size={14} strokeWidth={1.5} />,
344
246
  label: "Document Layout",
345
- handle: handleDocumentPDF,
247
+ handle: () =>
248
+ openExportDialog("pdf", {
249
+ pdf: { preset: "document" },
250
+ }),
346
251
  },
347
252
  {
348
253
  icon: <FileIcon size={14} strokeWidth={1.5} />,
349
254
  label: "Slides Layout",
350
- rightElement: renderRecommendedElement(true),
351
- hidden: !serverSidePdfEnabled,
352
- handle: async () => {
353
- await downloadServerSidePDF({
354
- preset: "slides",
355
- title: "Downloading Slides PDF...",
356
- });
357
- },
255
+ hidden: isWasm(),
256
+ rightElement: (
257
+ <span className="ml-3 shrink-0 rounded-full border border-emerald-200 bg-emerald-50 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-emerald-700">
258
+ Recommended
259
+ </span>
260
+ ),
261
+ handle: () =>
262
+ openExportDialog("pdf", {
263
+ pdf: { preset: "slides" },
264
+ }),
358
265
  },
359
266
  ],
360
267
  }
@@ -362,7 +269,10 @@ export function useNotebookActions() {
362
269
  divider: true,
363
270
  icon: <FileIcon size={14} strokeWidth={1.5} />,
364
271
  label: "Download as PDF",
365
- handle: handleDocumentPDF,
272
+ handle: () =>
273
+ openExportDialog("pdf", {
274
+ pdf: { preset: "document" },
275
+ }),
366
276
  },
367
277
  ],
368
278
  },
@@ -740,11 +650,3 @@ export function useNotebookActions() {
740
650
  return action;
741
651
  });
742
652
  }
743
-
744
- function toastNotebookMustBeNamed() {
745
- toast({
746
- title: "Error",
747
- description: "Notebooks must be named to be exported.",
748
- variant: "danger",
749
- });
750
- }
@@ -0,0 +1,26 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { describe, expect, it } from "vitest";
3
+ import { parseOutline } from "@/core/dom/outline";
4
+ import { findOutlineElements } from "../useActiveOutline";
5
+
6
+ describe("findOutlineElements", () => {
7
+ it.each([
8
+ "Design from the portfolio",
9
+ 'Decorative Art of "Spanish California"',
10
+ `The painter's "Study"`,
11
+ ])("finds an id-less heading named %s", (name) => {
12
+ const html = `<h3>${name}</h3>`;
13
+ document.body.innerHTML = html;
14
+
15
+ const outline = parseOutline({
16
+ mimetype: "text/html",
17
+ timestamp: 0,
18
+ channel: "output",
19
+ data: html,
20
+ });
21
+
22
+ expect(
23
+ findOutlineElements(outline?.items ?? []).map(([element]) => element),
24
+ ).toEqual([document.querySelector("h3")]);
25
+ });
26
+ });
@@ -87,7 +87,7 @@ export const AddConnectionDialogContent: React.FC<{
87
87
  <span className="block">{codeSnippetHint}</span>
88
88
  </DialogDescription>
89
89
  </DialogHeader>
90
- <AutoDiscoveredDataSources onSubmit={onClose} />
90
+ <AutoDiscoveredDataSources onSubmit={onClose} className="-mt-2" />
91
91
  <Tabs
92
92
  value={activeTab}
93
93
  onValueChange={(v) => setActiveTab(v as ConnectionTab)}
@@ -5,11 +5,13 @@ import { Tooltip, TooltipProvider } from "@/components/ui/tooltip";
5
5
  import type { DetectedDataSource } from "@/core/datasets/data-source-discovery";
6
6
  import { useDataSourceDiscovery } from "@/hooks/useDataSourceDiscovery";
7
7
  import { useInsertCode } from "./components";
8
+ import { cn } from "@/utils/cn";
8
9
 
9
10
  export const QuickAddDataSources: React.FC<{
11
+ className?: string;
10
12
  sources: DetectedDataSource[];
11
13
  onAdd: (source: DetectedDataSource) => void;
12
- }> = ({ sources, onAdd }) => {
14
+ }> = ({ className, sources, onAdd }) => {
13
15
  if (sources.length === 0) {
14
16
  return null;
15
17
  }
@@ -17,12 +19,15 @@ export const QuickAddDataSources: React.FC<{
17
19
  return (
18
20
  <section
19
21
  aria-labelledby="quick-add-data-sources-title"
20
- className="rounded-md border bg-muted/30 px-3 py-2"
22
+ className={cn(
23
+ "rounded-full bg-[linear-gradient(135deg,var(--blue-2),var(--purple-3))] px-3 py-2",
24
+ className,
25
+ )}
21
26
  >
22
27
  <div className="flex flex-wrap items-center gap-2">
23
28
  <div className="mr-1 flex items-center gap-1.5">
24
- <SparklesIcon className="h-3.5 w-3.5 text-muted-foreground" />
25
- <h3 id="quick-add-data-sources-title" className="text-sm font-medium">
29
+ <SparklesIcon className="h-3.5 w-3.5 text-(--blue-9)" />
30
+ <h3 id="quick-add-data-sources-title" className="text-sm">
26
31
  Quick add
27
32
  </h3>
28
33
  </div>
@@ -36,10 +41,10 @@ export const QuickAddDataSources: React.FC<{
36
41
  <button
37
42
  type="button"
38
43
  aria-label={`Add ${source.displayName} connection`}
39
- className="inline-flex items-center gap-1 rounded-full border border-(--blue-8) bg-(--blue-2) px-2.5 py-1 text-xs font-semibold text-(--blue-11) transition-colors hover:bg-(--blue-3) focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2"
44
+ className="inline-flex items-center gap-1 rounded-full border border-border/60 bg-background px-2.5 py-1 text-xs font-medium text-foreground/90 transition-colors hover:border-(--blue-7) focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2"
40
45
  onClick={() => onAdd(source)}
41
46
  >
42
- <PlusIcon className="h-3 w-3" />
47
+ <PlusIcon className="h-3 w-3 text-muted-foreground" />
43
48
  {source.displayName}
44
49
  </button>
45
50
  </Tooltip>
@@ -90,12 +95,14 @@ const DetectedDataSourceDetails: React.FC<{
90
95
 
91
96
  export const AutoDiscoveredDataSources: React.FC<{
92
97
  onSubmit: () => void;
93
- }> = ({ onSubmit }) => {
98
+ className?: string;
99
+ }> = ({ onSubmit, className }) => {
94
100
  const insertCode = useInsertCode();
95
101
  const { data } = useDataSourceDiscovery();
96
102
 
97
103
  return (
98
104
  <QuickAddDataSources
105
+ className={className}
99
106
  sources={data ?? []}
100
107
  onAdd={(source) => {
101
108
  insertCode(source.code);
@@ -154,6 +154,20 @@ _creds = json.loads(_credentials_json)
154
154
  fs = GoogleDriveFileSystem(creds=_creds, token="service_account", use_listings_cache=False, skip_instance_cache=True)"
155
155
  `;
156
156
 
157
+ exports[`generateStorageCode > Hugging Face > default connection 1`] = `
158
+ "from huggingface_hub import HfApi
159
+
160
+ hf = HfApi()"
161
+ `;
162
+
163
+ exports[`generateStorageCode > Hugging Face > with token from secrets 1`] = `
164
+ "from huggingface_hub import HfApi
165
+ import os
166
+
167
+ _token = os.environ.get("HF_TOKEN")
168
+ hf = HfApi(token=_token)"
169
+ `;
170
+
157
171
  exports[`generateStorageCode > S3 > basic connection with all fields 1`] = `
158
172
  "from obstore.store import S3Store
159
173
 
@@ -309,6 +309,26 @@ describe("generateStorageCode", () => {
309
309
  });
310
310
  });
311
311
 
312
+ describe("Hugging Face", () => {
313
+ it("default connection", () => {
314
+ expect(
315
+ generateStorageCode({ type: "huggingface" }, { library: "fsspec" }),
316
+ ).toMatchSnapshot();
317
+ });
318
+
319
+ it("with token from secrets", () => {
320
+ expect(
321
+ generateStorageCode(
322
+ {
323
+ type: "huggingface",
324
+ token: prefixSecret("HF_TOKEN"),
325
+ },
326
+ { library: "fsspec" },
327
+ ),
328
+ ).toMatchSnapshot();
329
+ });
330
+ });
331
+
312
332
  describe("invalid cases", () => {
313
333
  it("throws for empty S3 bucket", () => {
314
334
  expect(() =>
@@ -17,6 +17,7 @@ import {
17
17
  CoreWeaveStorageSchema,
18
18
  GCSStorageSchema,
19
19
  GoogleDriveStorageSchema,
20
+ HuggingfaceStorageSchema,
20
21
  S3StorageSchema,
21
22
  type StorageConnection,
22
23
  } from "./schemas";
@@ -79,6 +80,15 @@ const STORAGE_PROVIDERS = [
79
80
  preferred: "fsspec",
80
81
  },
81
82
  },
83
+ {
84
+ name: "Hugging Face Hub",
85
+ schema: HuggingfaceStorageSchema,
86
+ protocol: "hf",
87
+ storageLibraries: {
88
+ libraries: ["huggingface_hub"],
89
+ preferred: "huggingface_hub",
90
+ },
91
+ },
82
92
  ] satisfies StorageProviderSchema[];
83
93
 
84
94
  const StorageProviderSelector: React.FC<{
@@ -10,7 +10,7 @@ import {
10
10
  StorageConnectionSchema,
11
11
  } from "./schemas";
12
12
 
13
- export type StorageLibrary = "obstore" | "fsspec";
13
+ export type StorageLibrary = "obstore" | "fsspec" | "huggingface_hub";
14
14
 
15
15
  export interface StorageCodeOptions {
16
16
  library: StorageLibrary;
@@ -20,6 +20,7 @@ export interface StorageCodeOptions {
20
20
  export const StorageLibraryDisplayNames: Record<StorageLibrary, string> = {
21
21
  obstore: "obstore",
22
22
  fsspec: "fsspec",
23
+ huggingface_hub: "huggingface_hub",
23
24
  };
24
25
 
25
26
  class SecretContainer {
@@ -245,6 +246,20 @@ function generateGDriveCode(
245
246
  return { imports, code };
246
247
  }
247
248
 
249
+ function generateHuggingfaceCode(
250
+ connection: Extract<StorageConnection, { type: "huggingface" }>,
251
+ secrets: SecretContainer,
252
+ ): { imports: Set<string>; code: string } {
253
+ const imports = new Set(["from huggingface_hub import HfApi"]);
254
+
255
+ if (!connection.token) {
256
+ return { imports, code: "hf = HfApi()" };
257
+ }
258
+
259
+ const token = secrets.print("token", connection.token);
260
+ return { imports, code: `hf = HfApi(token=${token})` };
261
+ }
262
+
248
263
  export function generateStorageCode(
249
264
  connection: StorageConnection,
250
265
  options: StorageCodeOptions,
@@ -273,6 +288,9 @@ export function generateStorageCode(
273
288
  isEmbedded: options.isEmbedded,
274
289
  });
275
290
  break;
291
+ case "huggingface":
292
+ result = generateHuggingfaceCode(connection, secrets);
293
+ break;
276
294
  default:
277
295
  assertNever(connection);
278
296
  }
@@ -220,12 +220,31 @@ export const GoogleDriveStorageSchema = z
220
220
  })
221
221
  .describe(FieldOptions.of({ direction: "two-columns" }));
222
222
 
223
+ export const HuggingfaceStorageSchema = z
224
+ .object({
225
+ type: z.literal("huggingface"),
226
+ token: z
227
+ .string()
228
+ .optional()
229
+ .describe(
230
+ FieldOptions.of({
231
+ label: "Access Token",
232
+ description:
233
+ "Leave empty to use the HF_TOKEN environment variable or cached login",
234
+ inputType: "password",
235
+ optionRegex: "(hf.?token|hugging.?face.?token|hub.?token)",
236
+ }),
237
+ ),
238
+ })
239
+ .describe(FieldOptions.of({ direction: "two-columns" }));
240
+
223
241
  export const StorageConnectionSchema = z.discriminatedUnion("type", [
224
242
  S3StorageSchema,
225
243
  GCSStorageSchema,
226
244
  AzureStorageSchema,
227
245
  CoreWeaveStorageSchema,
228
246
  GoogleDriveStorageSchema,
247
+ HuggingfaceStorageSchema,
229
248
  ]);
230
249
 
231
250
  export type StorageConnection = z.infer<typeof StorageConnectionSchema>;