@marimo-team/islands 0.23.17-dev10 → 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/main.js CHANGED
@@ -22,17 +22,17 @@ import { _ as Functions, c as useEventListener, f as composeRefs, g as Logger, h
22
22
  import { t as require_react } from "./react-DA-nE2FX.js";
23
23
  import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
24
24
  import { n as Copy, r as toast, t as copyToClipboard } from "./copy-DNq8q3zw.js";
25
- import { $ as notebookOutline, $t as publishedCellClasses, A as ChevronDownIcon, An as Root2$1, At as DeferredRequestRegistry, Bn as Info, C as Accordion, Cn as atomWithReducer, Ct as MarkdownLanguageAdapter, D as BorderAllIcon, E as AccordionTrigger, Et as Paths, Fn as PaintRoller, G as hasRunAnyCellAtom, Gn as Columns2, H as renderHTML, Hn as Eye, Ht as DATA_TYPE_ICON, I as base64ToDataView, Jn as Braces, Jt as convertStatsName, K as cellIdsAtom, Lt as repl, Nn as Trash2, On as Content2, Pn as Table2, Pt as jotaiJsonStorage, Q as notebookAtom, Qt as isUninstantiated, R as dataViewToBase64, Rn as LoaderCircle, Rt as SQLParser, St as isRecord, T as AccordionItem, Tn as selectAtom, Tt as PathBuilder, U as getMarimoExportContext, V as safeExtractSetUIElementMessageBuffers, Vn as FileText, Vt as PluralWords, W as hasTrustedExportContext, Wn as Database, Wt as require_client, X as hasOnlyOneCellAtom, Xt as requestClientAtom, Y as getCellNames, Yt as getRequestClient, Z as initialNotebookState, Zt as useRequestClient, _n as UIElementId, _t as isInternalCellName, a as useCellFocusActions, at as createCell, bn as RANDOM_ID_ATTR, bt as customPythonLanguageSupport, cn as NotebookScopedLocalStorage, dn as parseAttrValue, dt as headingToIdentifier, en as shouldHidePublishedCell, et as numColumnsAtom, f as isOutputEmpty, fn as parseDataset, gt as getValidName, hn as HTMLCellId, i as LazyAnyLanguageCodeMirror, jn as Trigger2, jt as generateUUID, kn as Item$1, lt as outputIsLoading, n as Spinner, nt as useCellActions, o as useLastFocusedCellId, p as useExpandedConsoleOutput, pn as parseInitialValue, q as createActions, qn as CircleAlert, rt as useCellIds, s as maybeAddAltairImport, tt as reducer, ut as outputIsStale, vn as findCellId, vt as normalizeName, w as AccordionContent, wn as atomWithStorage, xn as jsonParseWithSpecialChar, xt as hasFunctionProperty, yn as OBJECT_ID_ATTR, yt as Checkbox, zn as Layers, zt as MarkdownParser, __tla as __tla_0 } from "./html-to-image-CZ1kLKkq.js";
25
+ import { $ as notebookOutline, $t as publishedCellClasses, A as ChevronDownIcon, An as Root2$1, At as DeferredRequestRegistry, Bn as Info, C as Accordion, Cn as atomWithReducer, Ct as MarkdownLanguageAdapter, D as BorderAllIcon, E as AccordionTrigger, Et as Paths, Fn as PaintRoller, G as hasRunAnyCellAtom, Gn as Columns2, H as renderHTML, Hn as Eye, Ht as DATA_TYPE_ICON, I as base64ToDataView, Jn as Braces, Jt as convertStatsName, K as cellIdsAtom, Lt as repl, Nn as Trash2, On as Content2, Pn as Table2, Pt as jotaiJsonStorage, Q as notebookAtom, Qt as isUninstantiated, R as dataViewToBase64, Rn as LoaderCircle, Rt as SQLParser, St as isRecord, T as AccordionItem, Tn as selectAtom, Tt as PathBuilder, U as getMarimoExportContext, V as safeExtractSetUIElementMessageBuffers, Vn as FileText, Vt as PluralWords, W as hasTrustedExportContext, Wn as Database, Wt as require_client, X as hasOnlyOneCellAtom, Xt as requestClientAtom, Y as getCellNames, Yt as getRequestClient, Z as initialNotebookState, Zt as useRequestClient, _n as UIElementId, _t as isInternalCellName, a as useCellFocusActions, at as createCell, bn as RANDOM_ID_ATTR, bt as customPythonLanguageSupport, cn as NotebookScopedLocalStorage, dn as parseAttrValue, dt as headingToIdentifier, en as shouldHidePublishedCell, et as numColumnsAtom, f as isOutputEmpty, fn as parseDataset, gt as getValidName, hn as HTMLCellId, i as LazyAnyLanguageCodeMirror, jn as Trigger2, jt as generateUUID, kn as Item$1, lt as outputIsLoading, n as Spinner, nt as useCellActions, o as useLastFocusedCellId, p as useExpandedConsoleOutput, pn as parseInitialValue, q as createActions, qn as CircleAlert, rt as useCellIds, s as maybeAddAltairImport, tt as reducer, ut as outputIsStale, vn as findCellId, vt as normalizeName, w as AccordionContent, wn as atomWithStorage, xn as jsonParseWithSpecialChar, xt as hasFunctionProperty, yn as OBJECT_ID_ATTR, yt as Checkbox, zn as Layers, zt as MarkdownParser, __tla as __tla_0 } from "./html-to-image-CxlSazqu.js";
26
26
  import { __tla as __tla_1 } from "./chunk-5FQGJX7Z-B9WoXK1R.js";
27
27
  import { o as useSize, s as Root$1, u as createLucideIcon } from "./dist-DIlxLFXG.js";
28
28
  import { A as SquareFunction, C as DEFAULT_COLOR_SCHEME, D as SCALE_TYPE_DESCRIPTIONS, E as EMPTY_VALUE$1, O as TIME_UNIT_DESCRIPTIONS, S as DEFAULT_AGGREGATION, T as DEFAULT_TIME_UNIT, _ as AGGREGATION_TYPE_DESCRIPTIONS, a as AGGREGATION_FNS$1, b as COLOR_SCHEMES, c as COLOR_BY_FIELDS, d as NONE_VALUE, f as SELECTABLE_DATA_TYPES, g as TIME_UNITS, h as STRING_AGGREGATION_FNS, i as convertDataTypeToSelectable, j as ChartColumn, k as escapeFieldName, l as COMBINED_TIME_UNITS, m as SORT_TYPES, n as createSpecWithoutData, o as BIN_AGGREGATION, p as SINGLE_TIME_UNITS, r as isFieldSet, s as CHART_TYPES, t as augmentSpecWithData, u as ChartType, v as AGGREGATION_TYPE_ICON, w as DEFAULT_MAX_BINS_FACET, x as COUNT_FIELD, y as CHART_TYPE_ICON } from "./spec-CQKL_D3i.js";
29
- import { $ as CommandSeparator, $t as Kbd, A as ErrorState, At as getPageIndexForRow, B as ContextMenuTrigger, Bt as dateToLocalISODate, C as downloadHTMLAsImage, Ct as inferFieldTypes, D as ColumnName, Dt as DelayMount, E as downloadSizeLimitAtom, Et as ColumnChartSpecModel, F as getUserColumnVisibilityCounts, Ft as TOO_MANY_ROWS, G as DateRangePicker, Gt as TabsList, H as useSelectList, Ht as dateToLocalISOTime, I as ContextMenu, It as toFieldTypes, J as Command, Jt as ChartInfoState, K as Combobox, Kt as TabsTrigger, L as ContextMenuContent, Lt as getMimeValues, M as prettifyRowColumnCount, Mt as loadTableData, N as prettifyRowCount, Nt as INDEX_COLUMN_NAME, O as ColumnPreviewContainer, Ot as useIntersectionObserver, P as getColumnCountForDisplay, Pt as SELECT_COLUMN_ID, Q as CommandList, Qt as RenderTextWithLinks, R as ContextMenuItem, Rt as isNullishFilter, S as downloadExportedFile, St as generateColumns, T as Filenames, Tt as ColumnChartContext, U as CompactChipRow, Ut as Tabs, V as useInternalStateWithSync, Vt as dateToLocalISODateTime, W as DatePicker, Wt as TabsContent, X as CommandInput, Xt as LazyVegaEmbed, Y as CommandEmpty, Yt as ChartLoadingState, Z as CommandItem, Zt as useOverflowDetection, _ as DataTable, _n as ChevronLeft, _t as TableCell, an as TextWrap, at as contextAwarePanelType, b as downloadBlob, bt as TableRow, c as SLIDE_TYPE_OPTIONS_BY_VALUE, cn as EyeOff, ct as slotsController, d as Switch, dn as Download, en as HtmlOutput, et as smartMatch, f as RadioGroup, fn as Code, g as OutputRenderer, gn as ChevronsDownUp, gt as TableBody, h as OutputArea, hn as ChevronsLeft, ht as Table, i as showCodeInRunModeAtom, in as $fae977aafc393c5c$export$6b862160d295c8e, it as contextAwarePanelOwner, j as LoadingState, jt as loadTableAndRawData, k as EmptyState, kt as usePrevious$1, lt as Toggle, m as JsonOutput, mn as ChevronsRight, mt as Provider$1, n as useNotebookCodeAvailable, nn as safeLocale, nt as PANEL_TYPES, on as GripHorizontal, ot as isCellAwareAtom, p as RadioGroupItem, pn as ChevronsUpDown, pt as Fill, q as ComboboxItem, qt as ChartErrorState, r as marimoVersionAtom, rn as $fae977aafc393c5c$export$588937bcd60ade55, rt as contextAwarePanelOpen, sn as Funnel, st as SlotNames, t as cellDomProps, tn as EmotionCacheProvider, tt as ContextAwarePanelItem, u as Slide, un as Ellipsis, v as InstallPackageButton, vn as ArrowDownWideNarrow, vt as TableHead, w as Progress, wt as renderCellValue, x as downloadByURL, xt as NAMELESS_COLUMN_PREFIX, y as ADD_PRINTING_CLASS, yt as TableHeader, z as ContextMenuSeparator, zt as Maps, __tla as __tla_2 } from "./common-CL5RzjRX.js";
29
+ import { $ as CommandSeparator, $t as Kbd, A as ErrorState, At as getPageIndexForRow, B as ContextMenuTrigger, Bt as dateToLocalISODate, C as downloadHTMLAsImage, Ct as inferFieldTypes, D as ColumnName, Dt as DelayMount, E as downloadSizeLimitAtom, Et as ColumnChartSpecModel, F as getUserColumnVisibilityCounts, Ft as TOO_MANY_ROWS, G as DateRangePicker, Gt as TabsList, H as useSelectList, Ht as dateToLocalISOTime, I as ContextMenu, It as toFieldTypes, J as Command, Jt as ChartInfoState, K as Combobox, Kt as TabsTrigger, L as ContextMenuContent, Lt as getMimeValues, M as prettifyRowColumnCount, Mt as loadTableData, N as prettifyRowCount, Nt as INDEX_COLUMN_NAME, O as ColumnPreviewContainer, Ot as useIntersectionObserver, P as getColumnCountForDisplay, Pt as SELECT_COLUMN_ID, Q as CommandList, Qt as RenderTextWithLinks, R as ContextMenuItem, Rt as isNullishFilter, S as downloadExportedFile, St as generateColumns, T as Filenames, Tt as ColumnChartContext, U as CompactChipRow, Ut as Tabs, V as useInternalStateWithSync, Vt as dateToLocalISODateTime, W as DatePicker, Wt as TabsContent, X as CommandInput, Xt as LazyVegaEmbed, Y as CommandEmpty, Yt as ChartLoadingState, Z as CommandItem, Zt as useOverflowDetection, _ as DataTable, _n as ChevronLeft, _t as TableCell, an as TextWrap, at as contextAwarePanelType, b as downloadBlob, bt as TableRow, c as SLIDE_TYPE_OPTIONS_BY_VALUE, cn as EyeOff, ct as slotsController, d as Switch, dn as Download, en as HtmlOutput, et as smartMatch, f as RadioGroup, fn as Code, g as OutputRenderer, gn as ChevronsDownUp, gt as TableBody, h as OutputArea, hn as ChevronsLeft, ht as Table, i as showCodeInRunModeAtom, in as $fae977aafc393c5c$export$6b862160d295c8e, it as contextAwarePanelOwner, j as LoadingState, jt as loadTableAndRawData, k as EmptyState, kt as usePrevious$1, lt as Toggle, m as JsonOutput, mn as ChevronsRight, mt as Provider$1, n as useNotebookCodeAvailable, nn as safeLocale, nt as PANEL_TYPES, on as GripHorizontal, ot as isCellAwareAtom, p as RadioGroupItem, pn as ChevronsUpDown, pt as Fill, q as ComboboxItem, qt as ChartErrorState, r as marimoVersionAtom, rn as $fae977aafc393c5c$export$588937bcd60ade55, rt as contextAwarePanelOpen, sn as Funnel, st as SlotNames, t as cellDomProps, tn as EmotionCacheProvider, tt as ContextAwarePanelItem, u as Slide, un as Ellipsis, v as InstallPackageButton, vn as ArrowDownWideNarrow, vt as TableHead, w as Progress, wt as renderCellValue, x as downloadByURL, xt as NAMELESS_COLUMN_PREFIX, y as ADD_PRINTING_CLASS, yt as TableHeader, z as ContextMenuSeparator, zt as Maps, __tla as __tla_2 } from "./common-D8DGPf0N.js";
30
30
  import { c as Calendar, i as createReducerAndAtoms, n as useOnUnmount, o as ToggleLeft, t as useOnMount } from "./useLifecycle-CMJGqBaY.js";
31
31
  import { t as Check } from "./check-DP7U0H0G.js";
32
32
  import { A as Icon, C as logNever, D as $18f2051aff69b9bf$export$a54013f0d02a8f82, E as $18f2051aff69b9bf$export$43bb16f9c6d9e3f7, F as createCollection, I as X, M as clamp$2, N as usePrevious$2, P as useDirection, R as ChevronDown, S as assertNever, a as SelectGroup, c as SelectSeparator, d as NativeSelect, i as SelectContent, j as Trigger$1, l as SelectTrigger, n as capitalize, o as SelectItem, r as Select, s as SelectLabel, t as Strings, u as SelectValue, w as $a916eb452884faea$export$b7a616150fdb9f44 } from "./strings-DIy9NAmg.js";
33
33
  import { B as $64fa3d84918910a7$export$4d86445c2cf5e3, C as DropdownMenuTrigger, Ft as $65484d02dcb7eb3e$export$457c3d6518dd4c6f, It as $3ef42575df84b30b$export$9d1611c77c2fe928, W as $64fa3d84918910a7$export$df3a06d6289f983e, Wt as $ff5963eb1fccf552$export$e08e3b67e392101e, X as useDebouncedCallback, Y as useDebounceControlledState, a as NumberField, d as roundToFractionDigits, f as DropdownMenu, g as DropdownMenuLabel, gn as ChevronRight, h as DropdownMenuItem, hn as Circle, i as OnBlurredInput, l as prettyNumber, m as DropdownMenuGroup, n as DebouncedNumberInput, o as maxFractionDigitsForSteps, ot as $f7dceffc5ad7768b$export$4e328f61c538687f, p as DropdownMenuContent, pt as $6179b936705e76d3$export$ae780daf29e6d456, r as Input, st as $701a24aa0da5b062$export$ea18c227d4417cc3, t as DebouncedInput, u as prettyScientificNumber, v as DropdownMenuSeparator, xt as $458b0a5536c1a7cf$export$40bfa8c7b0832715, y as DropdownMenuShortcut, z as $64fa3d84918910a7$export$29f1550f4b0d4415 } from "./input-C5K4VBvB.js";
34
34
  import { S as isWasm, _ as initialModeAtom, b as viewStateAtom, c as asRemoteURL, d as isStaticNotebook, f as appendQueryParams, g as getInitialAppMode, m as require_cuid2, u as getStaticVirtualFiles, v as kioskModeAtom, w as CircleQuestionMark, x as Deferred } from "./toDate-CqhBWjvO.js";
35
- import { a as MarimoIncomingMessageEvent, c as MarimoValueUpdateEvent, f as Square, i as PythonIcon, l as createInputEvent, n as blobToString, o as MarimoValueInputEvent, p as File, r as filesToBase64, s as MarimoValueReadyEvent, t as processOutput, u as deserializeBlob } from "./process-output-bVfbcx5_.js";
35
+ import { a as MarimoIncomingMessageEvent, c as MarimoValueUpdateEvent, f as Square, i as PythonIcon, l as createInputEvent, n as blobToString, o as MarimoValueInputEvent, p as File, r as filesToBase64, s as MarimoValueReadyEvent, t as processOutput, u as deserializeBlob } from "./process-output-B24cDGzV.js";
36
36
  import { n as Trash, r as Pencil, t as BulkEdit } from "./types-DicH9Oi0.js";
37
37
  import { n as require_prop_types, r as Plus, t as ErrorBoundary } from "./ErrorBoundary-DhLqwSWA.js";
38
38
  import { t as require_react_dom } from "./react-dom-BTJzcVJ9.js";
@@ -9017,7 +9017,7 @@ let __tla = Promise.all([
9017
9017
  };
9018
9018
  }
9019
9019
  };
9020
- var LazyChatbot = import_react.lazy(() => import("./chat-ui-DsZJj75A.js").then((e) => ({
9020
+ var LazyChatbot = import_react.lazy(() => import("./chat-ui-DH3hwWoI.js").then((e) => ({
9021
9021
  default: e.Chatbot
9022
9022
  }))), messageSchema = array(object({
9023
9023
  id: string(),
@@ -38218,7 +38218,7 @@ ${c}
38218
38218
  function _temp2$2(e) {
38219
38219
  e.target === e.currentTarget && e.key === "Enter" && (e.preventDefault(), e.stopPropagation(), e.currentTarget.click());
38220
38220
  }
38221
- var LazySlidesComponent = import_react.lazy(() => import("./reveal-component-o0NFIMKL.js"));
38221
+ var LazySlidesComponent = import_react.lazy(() => import("./reveal-component-UfXDaVe-.js"));
38222
38222
  const SlidesLayoutRenderer = ({ layout: e, setLayout: r, cells: c, mode: l }) => {
38223
38223
  var _a5;
38224
38224
  let u = useAtomValue(kioskModeAtom), d = l === "read" || u, f = useAtomValue(numColumnsAtom) > 1, [p, m] = (0, import_react.useState)(null), { slideCells: h, skippedIds: g, noOutputIds: _, slideTypes: v, startCellIndex: y } = (0, import_react.useMemo)(() => computeSlideCellsInfo(c, e), [
@@ -1,6 +1,6 @@
1
1
  import { s as __toESM } from "./chunk-BNovOVIE.js";
2
2
  import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
3
- import { F as base64ToDataURL, ot as ansiToPlainText, st as parseHtmlContent } from "./html-to-image-CZ1kLKkq.js";
3
+ import { F as base64ToDataURL, ot as ansiToPlainText, st as parseHtmlContent } from "./html-to-image-CxlSazqu.js";
4
4
  import { u as createLucideIcon } from "./dist-DIlxLFXG.js";
5
5
  import { t as Strings } from "./strings-DIy9NAmg.js";
6
6
  import { t as require_jsx_runtime } from "./jsx-runtime-DebpN0FN.js";
@@ -6,10 +6,10 @@ import { s as __toESM } from "./chunk-BNovOVIE.js";
6
6
  import { c as useEventListener, g as Logger, h as cn, m as Events, t as Button } from "./button-BSm8IXRU.js";
7
7
  import { t as require_react } from "./react-DA-nE2FX.js";
8
8
  import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
9
- import { ut as outputIsStale } from "./html-to-image-CZ1kLKkq.js";
9
+ import { ut as outputIsStale } from "./html-to-image-CxlSazqu.js";
10
10
  import "./chunk-5FQGJX7Z-B9WoXK1R.js";
11
11
  import { u as createLucideIcon } from "./dist-DIlxLFXG.js";
12
- import { a as DEFAULT_DECK_TRANSITION, cn as EyeOff, dt as PanelGroup, fn as Code, ft as PanelResizeHandle, l as SlideSidebar, ln as Expand, n as useNotebookCodeAvailable, o as DEFAULT_DECK_VERTICAL_ALIGN, s as DEFAULT_SLIDE_TYPE, t as cellDomProps, u as Slide, ut as Panel } from "./common-CL5RzjRX.js";
12
+ import { a as DEFAULT_DECK_TRANSITION, cn as EyeOff, dt as PanelGroup, fn as Code, ft as PanelResizeHandle, l as SlideSidebar, ln as Expand, n as useNotebookCodeAvailable, o as DEFAULT_DECK_VERTICAL_ALIGN, s as DEFAULT_SLIDE_TYPE, t as cellDomProps, u as Slide, ut as Panel } from "./common-D8DGPf0N.js";
13
13
  import { R as ChevronDown } from "./strings-DIy9NAmg.js";
14
14
  import { X as useDebouncedCallback } from "./input-C5K4VBvB.js";
15
15
  import { v as kioskModeAtom } from "./toDate-CqhBWjvO.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.23.17-dev10",
3
+ "version": "0.23.17-dev13",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -72,26 +72,31 @@ export const MockRequestClient = {
72
72
  format: "html",
73
73
  dependenciesAvailable: true,
74
74
  missingPackages: [],
75
+ missingSetup: [],
75
76
  },
76
77
  {
77
78
  format: "markdown",
78
79
  dependenciesAvailable: true,
79
80
  missingPackages: [],
81
+ missingSetup: [],
80
82
  },
81
83
  {
82
84
  format: "ipynb",
83
85
  dependenciesAvailable: true,
84
86
  missingPackages: [],
87
+ missingSetup: [],
85
88
  },
86
89
  {
87
90
  format: "pdf",
88
91
  dependenciesAvailable: true,
89
92
  missingPackages: [],
93
+ missingSetup: [],
90
94
  },
91
95
  {
92
96
  format: "script",
93
97
  dependenciesAvailable: true,
94
98
  missingPackages: [],
99
+ missingSetup: [],
95
100
  },
96
101
  ],
97
102
  }),
@@ -259,6 +259,7 @@ describe("ExportDialog", () => {
259
259
  format: "pdf",
260
260
  dependenciesAvailable: false,
261
261
  missingPackages: ["nbconvert[webpdf]"],
262
+ missingSetup: [],
262
263
  },
263
264
  ],
264
265
  }),
@@ -275,6 +276,40 @@ describe("ExportDialog", () => {
275
276
  expect(screen.getByTestId("export-format-pdf")).toBeVisible();
276
277
  });
277
278
 
279
+ it("explains missing Playwright Chromium setup", async () => {
280
+ store.set(
281
+ requestClientAtom,
282
+ MockRequestClient.create({
283
+ getExportAvailability: vi.fn().mockResolvedValue({
284
+ source: "server",
285
+ formats: [
286
+ {
287
+ format: "pdf",
288
+ dependenciesAvailable: false,
289
+ missingPackages: [],
290
+ missingSetup: [
291
+ {
292
+ name: "playwright-chromium",
293
+ command: "uv run playwright install chromium",
294
+ },
295
+ ],
296
+ },
297
+ ],
298
+ }),
299
+ }),
300
+ );
301
+
302
+ renderDialog("pdf");
303
+
304
+ expect(
305
+ await screen.findByText("PDF export requires Playwright Chromium."),
306
+ ).toBeVisible();
307
+ expect(
308
+ screen.getByText("uv run playwright install chromium"),
309
+ ).toBeVisible();
310
+ expect(screen.getByTestId("export-submit")).toBeDisabled();
311
+ });
312
+
278
313
  it("announces requirement checks as status updates", () => {
279
314
  store.set(
280
315
  requestClientAtom,
@@ -19,11 +19,13 @@ const AVAILABLE: ExportAvailabilityResponse = {
19
19
  format: "ipynb",
20
20
  dependenciesAvailable: true,
21
21
  missingPackages: [],
22
+ missingSetup: [],
22
23
  },
23
24
  {
24
25
  format: "pdf",
25
26
  dependenciesAvailable: true,
26
27
  missingPackages: [],
28
+ missingSetup: [],
27
29
  },
28
30
  ],
29
31
  };
@@ -188,6 +190,7 @@ describe("getExportFormatStatus", () => {
188
190
  format: "pdf",
189
191
  dependenciesAvailable: false,
190
192
  missingPackages: ["nbconvert[webpdf]"],
193
+ missingSetup: [],
191
194
  },
192
195
  ],
193
196
  },
@@ -202,6 +205,64 @@ describe("getExportFormatStatus", () => {
202
205
  });
203
206
  });
204
207
 
208
+ it("reports missing server setup", () => {
209
+ expect(
210
+ status("pdf", {
211
+ availability: {
212
+ status: "success",
213
+ data: {
214
+ source: "server",
215
+ formats: [
216
+ {
217
+ format: "pdf",
218
+ dependenciesAvailable: false,
219
+ missingPackages: [],
220
+ missingSetup: [
221
+ {
222
+ name: "playwright-chromium",
223
+ command: "uv run playwright install chromium",
224
+ },
225
+ ],
226
+ },
227
+ ],
228
+ },
229
+ },
230
+ }),
231
+ ).toEqual({
232
+ available: false,
233
+ reason: {
234
+ type: "missing-setup",
235
+ requirements: [
236
+ {
237
+ name: "playwright-chromium",
238
+ command: "uv run playwright install chromium",
239
+ },
240
+ ],
241
+ },
242
+ });
243
+ });
244
+
245
+ it("fails open when an unavailable format has no requirements", () => {
246
+ expect(
247
+ status("pdf", {
248
+ availability: {
249
+ status: "success",
250
+ data: {
251
+ source: "server",
252
+ formats: [
253
+ {
254
+ format: "pdf",
255
+ dependenciesAvailable: false,
256
+ missingPackages: [],
257
+ missingSetup: [],
258
+ },
259
+ ],
260
+ },
261
+ },
262
+ }),
263
+ ).toEqual({ available: true, availabilityCheckFailed: true });
264
+ });
265
+
205
266
  it.each([
206
267
  ["html", true, undefined],
207
268
  ["markdown", true, undefined],
@@ -10,8 +10,18 @@ import type {
10
10
  ExportBlockReason,
11
11
  ExportFormat,
12
12
  ExportFormatStatus,
13
+ ExportSetupRequirement,
13
14
  } from "./state";
14
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
+
15
25
  export function FormatStatusIcon({ status }: { status: ExportFormatStatus }) {
16
26
  if (status.available && !status.availabilityCheckFailed) {
17
27
  return null;
@@ -124,6 +134,24 @@ function ReasonNotice({
124
134
  where marimo is running to use this export.
125
135
  </Notice>
126
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
+ );
127
155
  case "wasm-runtime":
128
156
  return (
129
157
  <Notice>
@@ -15,6 +15,8 @@ export const EXPORT_FORMATS = [
15
15
  ] as const;
16
16
 
17
17
  export type ExportFormat = (typeof EXPORT_FORMATS)[number];
18
+ export type ExportSetupRequirement =
19
+ ExportAvailabilityResponse["formats"][number]["missingSetup"][number];
18
20
 
19
21
  export const MARKDOWN_FLAVORS = ["pymdown", "qmd", "mystmd", "mdx"] as const;
20
22
  export const IPYNB_SORT_MODES = ["topological", "top-down"] as const;
@@ -185,6 +187,7 @@ export type ExportBlockReason =
185
187
  | { type: "checking-requirements" }
186
188
  | { type: "notebook-must-be-named" }
187
189
  | { type: "missing-packages"; packages: string[] }
190
+ | { type: "missing-setup"; requirements: ExportSetupRequirement[] }
188
191
  | { type: "wasm-runtime" };
189
192
 
190
193
  export interface ExportFormatStatus {
@@ -308,12 +311,27 @@ export function getExportFormatStatus({
308
311
  return { available: true, availabilityCheckFailed: true };
309
312
  }
310
313
  if (!serverStatus.dependenciesAvailable) {
314
+ if (serverStatus.missingPackages.length > 0) {
315
+ return {
316
+ available: false,
317
+ reason: {
318
+ type: "missing-packages",
319
+ packages: serverStatus.missingPackages,
320
+ },
321
+ };
322
+ }
323
+ if (serverStatus.missingSetup.length > 0) {
324
+ return {
325
+ available: false,
326
+ reason: {
327
+ type: "missing-setup",
328
+ requirements: serverStatus.missingSetup,
329
+ },
330
+ };
331
+ }
311
332
  return {
312
- available: false,
313
- reason: {
314
- type: "missing-packages",
315
- packages: serverStatus.missingPackages,
316
- },
333
+ available: true,
334
+ availabilityCheckFailed: true,
317
335
  };
318
336
  }
319
337
 
@@ -1,13 +1,13 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { useAtom, useAtomValue, useSetAtom } from "jotai";
3
+ import { useAtom } from "jotai";
4
4
  import { useEffect, useRef, useState } from "react";
5
5
  import { toast } from "@/components/ui/use-toast";
6
6
  import {
7
7
  updateCellOutputsWithScreenshots,
8
8
  useEnrichCellOutputs,
9
9
  } from "@/core/export/hooks";
10
- import { runDuringPresentMode, viewStateAtom } from "@/core/mode";
10
+ import { runDuringPresentMode } from "@/core/mode";
11
11
  import { useRequestClient } from "@/core/network/requests";
12
12
  import type { ExportAvailabilityResponse } from "@/core/network/types";
13
13
  import { useFilename } from "@/core/saving/filename";
@@ -278,8 +278,6 @@ function useExportDialogAction({
278
278
  }) {
279
279
  const requests = useRequestClient();
280
280
  const takeScreenshots = useEnrichCellOutputs();
281
- const viewState = useAtomValue(viewStateAtom);
282
- const setViewState = useSetAtom(viewStateAtom);
283
281
  const [isExporting, setIsExporting] = useState(false);
284
282
  const dialogRef = useRef<HTMLDivElement>(null);
285
283
  const mountedRef = useRef(true);
@@ -303,15 +301,7 @@ function useExportDialogAction({
303
301
  const capturePNG = async () => {
304
302
  const capture = () =>
305
303
  captureCurrentAppView(dialogRef.current?.parentElement ?? null);
306
- if (viewState.mode !== "edit") {
307
- await capture();
308
- return;
309
- }
310
- try {
311
- await runDuringPresentMode(capture);
312
- } finally {
313
- setViewState(viewState);
314
- }
304
+ await runDuringPresentMode(capture);
315
305
  };
316
306
 
317
307
  const submit = async () => {
@@ -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
+ });
@@ -0,0 +1,85 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
+ import { CellId } from "@/core/cells/ids";
5
+ import { runDuringPresentMode, viewStateAtom } from "@/core/mode";
6
+ import { store } from "@/core/state/jotai";
7
+
8
+ const requestAnimationFrameMock = vi.fn((callback: FrameRequestCallback) => {
9
+ callback(0);
10
+ return 0;
11
+ });
12
+
13
+ async function runAfterRender(fn: () => void | Promise<void>): Promise<void> {
14
+ const result = runDuringPresentMode(fn);
15
+ await vi.runAllTimersAsync();
16
+ return result;
17
+ }
18
+
19
+ describe("runDuringPresentMode", () => {
20
+ beforeEach(() => {
21
+ vi.useFakeTimers();
22
+ vi.stubGlobal("requestAnimationFrame", requestAnimationFrameMock);
23
+ requestAnimationFrameMock.mockClear();
24
+ });
25
+
26
+ afterEach(() => {
27
+ vi.useRealTimers();
28
+ vi.unstubAllGlobals();
29
+ });
30
+
31
+ it("runs in present mode and restores the captured view state", async () => {
32
+ const state = { mode: "edit" as const, cellAnchor: CellId.create() };
33
+ store.set(viewStateAtom, state);
34
+
35
+ await runAfterRender(() => {
36
+ expect(store.get(viewStateAtom)).toEqual({
37
+ ...state,
38
+ mode: "present",
39
+ });
40
+ });
41
+
42
+ expect(store.get(viewStateAtom)).toEqual(state);
43
+ expect(requestAnimationFrameMock).toHaveBeenCalledTimes(2);
44
+ });
45
+
46
+ it("restores the captured view state when the callback rejects", async () => {
47
+ const state = { mode: "edit" as const, cellAnchor: CellId.create() };
48
+ const error = new Error("capture failed");
49
+ store.set(viewStateAtom, state);
50
+
51
+ const result = runDuringPresentMode(() => Promise.reject(error));
52
+ const rejection = expect(result).rejects.toBe(error);
53
+ await vi.runAllTimersAsync();
54
+ await rejection;
55
+
56
+ expect(store.get(viewStateAtom)).toEqual(state);
57
+ });
58
+
59
+ it("runs directly when already in present mode", async () => {
60
+ const state = { mode: "present" as const, cellAnchor: CellId.create() };
61
+ store.set(viewStateAtom, state);
62
+
63
+ await runDuringPresentMode(() => {
64
+ expect(store.get(viewStateAtom)).toEqual(state);
65
+ });
66
+
67
+ expect(store.get(viewStateAtom)).toEqual(state);
68
+ expect(requestAnimationFrameMock).not.toHaveBeenCalled();
69
+ });
70
+
71
+ it.each(["read", "home", "gallery"] as const)(
72
+ "runs directly without changing %s mode",
73
+ async (mode) => {
74
+ const state = { mode, cellAnchor: CellId.create() };
75
+ store.set(viewStateAtom, state);
76
+
77
+ await runDuringPresentMode(() => {
78
+ expect(store.get(viewStateAtom)).toEqual(state);
79
+ });
80
+
81
+ expect(store.get(viewStateAtom)).toEqual(state);
82
+ expect(requestAnimationFrameMock).not.toHaveBeenCalled();
83
+ },
84
+ );
85
+ });
@@ -63,13 +63,36 @@ function getOutline(html: string): Outline | null {
63
63
  return { items };
64
64
  }
65
65
 
66
+ function toXPathStringLiteral(value: string): string {
67
+ if (!value.includes('"')) {
68
+ return `"${value}"`;
69
+ }
70
+ if (!value.includes("'")) {
71
+ return `'${value}'`;
72
+ }
73
+
74
+ const segments = value.split('"');
75
+ const literals: string[] = [];
76
+ for (const [index, segment] of segments.entries()) {
77
+ if (segment) {
78
+ literals.push(`"${segment}"`);
79
+ }
80
+ if (index < segments.length - 1) {
81
+ literals.push(`'"'`);
82
+ }
83
+ }
84
+ return `concat(${literals.join(", ")})`;
85
+ }
86
+
66
87
  export function headingToIdentifier(heading: Element): OutlineItem["by"] {
67
88
  const id = heading.id;
68
89
  if (id) {
69
90
  return { id };
70
91
  }
71
- const name = heading.textContent;
72
- return { path: `//${heading.tagName}[contains(., "${name}")]` };
92
+ const name = heading.textContent ?? "";
93
+ return {
94
+ path: `//${heading.tagName}[contains(., ${toXPathStringLiteral(name)})]`,
95
+ };
73
96
  }
74
97
 
75
98
  export function mergeOutlines(outlines: (Outline | null)[]): Outline {
package/src/core/mode.ts CHANGED
@@ -55,20 +55,22 @@ export async function runDuringPresentMode(
55
55
  fn: () => void | Promise<void>,
56
56
  ): Promise<void> {
57
57
  const state = store.get(viewStateAtom);
58
- if (state.mode === "present") {
58
+ if (state.mode !== "edit") {
59
59
  await fn();
60
60
  return;
61
61
  }
62
62
 
63
63
  store.set(viewStateAtom, { ...state, mode: "present" });
64
- // Wait 100ms to allow the page to render
65
- await new Promise((resolve) => setTimeout(resolve, 100));
66
- // Wait 2 frames
67
- await new Promise((resolve) => requestAnimationFrame(resolve));
68
- await new Promise((resolve) => requestAnimationFrame(resolve));
69
- await fn();
70
- store.set(viewStateAtom, { ...state, mode: "edit" });
71
- return undefined;
64
+ try {
65
+ // Wait 100ms to allow the page to render
66
+ await new Promise((resolve) => setTimeout(resolve, 100));
67
+ // Wait 2 frames
68
+ await new Promise((resolve) => requestAnimationFrame(resolve));
69
+ await new Promise((resolve) => requestAnimationFrame(resolve));
70
+ await fn();
71
+ } finally {
72
+ store.set(viewStateAtom, state);
73
+ }
72
74
  }
73
75
 
74
76
  export const viewStateAtom = atom<ViewState>({
@@ -84,6 +84,7 @@ describe("createLazyRequests", () => {
84
84
  format: "ipynb",
85
85
  dependenciesAvailable: false,
86
86
  missingPackages: ["nbformat"],
87
+ missingSetup: [],
87
88
  },
88
89
  ],
89
90
  };