@marimo-team/frontend 0.24.1-dev80 → 0.24.1-dev82
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/assets/{CellStatus-DMQfH9wD.js → CellStatus-GcHBZKLm.js} +1 -1
- package/dist/assets/{JsonOutput-B7sDhGzR.js → JsonOutput-CUJlEdsN.js} +8 -8
- package/dist/assets/{MarimoErrorOutput-C0vk393J.js → MarimoErrorOutput-DCAezqcJ.js} +2 -2
- package/dist/assets/{RenderHTML-BnMaGpV8.js → RenderHTML-D9-75ltP.js} +1 -1
- package/dist/assets/{RunButton-nocIODpS.js → RunButton-plSsbubG.js} +1 -1
- package/dist/assets/{add-cell-with-ai-B_TSaqjJ.js → add-cell-with-ai-Cfby28hZ.js} +14 -14
- package/dist/assets/{add-connection-dialog-DOuojDa-.js → add-connection-dialog-By0i0Fq5.js} +42 -42
- package/dist/assets/{agent-panel-CkKCGjDp.js → agent-panel-Cf482jaA.js} +4 -4
- package/dist/assets/ai-model-dropdown-DMfaGQ8o.js +3 -0
- package/dist/assets/app-config-button-93unbEYx.js +1 -0
- package/dist/assets/{blob-CYM8xQsK.js → blob-yxfhyyvD.js} +1 -1
- package/dist/assets/cell-editor-CZmqG6aB.js +21 -0
- package/dist/assets/{cell-link-DCOAqVkE.js → cell-link-hHrWcozB.js} +1 -1
- package/dist/assets/{cells-C6lBv8kg.js → cells-yuYDzbAy.js} +51 -51
- package/dist/assets/{chat-display-B6GBZDq8.js → chat-display-CnhvADgL.js} +1 -1
- package/dist/assets/{chat-panel-wWuBXhXw.js → chat-panel-CdE97AxM.js} +2 -2
- package/dist/assets/{chat-ui-CQ7A3qdB.js → chat-ui-BD3rpULw.js} +2 -2
- package/dist/assets/{column-preview-CatwdUda.js → column-preview-BEmcYaKl.js} +1 -1
- package/dist/assets/{command-palette-DcPM_WiO.js → command-palette-BGRRw0sn.js} +1 -1
- package/dist/assets/{common-B4aMxphz.js → common-Bs2kaOs2.js} +1 -1
- package/dist/assets/{components-D8-ffQsq.js → components-ByyAzQxP.js} +1 -1
- package/dist/assets/{datasource-D-_jFtGs.js → datasource-DeZYq5UY.js} +2 -2
- package/dist/assets/{dependency-graph-panel-BQli6zxh.js → dependency-graph-panel-g10osAFc.js} +4 -4
- package/dist/assets/{documentation-panel-_QE3nPOg.js → documentation-panel-CZ1mKkDh.js} +1 -1
- package/dist/assets/{download-Cqjqrhzo.js → download-D1crMjD8.js} +1 -1
- package/dist/assets/edit-page-Bk1sT7Wb.js +9 -0
- package/dist/assets/{error-entries-c5sIySli.js → error-entries-B6XTLRyE.js} +1 -1
- package/dist/assets/{error-panel-CXKrWK_u.js → error-panel-lexB3L6f.js} +1 -1
- package/dist/assets/{file-explorer-panel-BZiAb4x_.js → file-explorer-panel-DHRH2J-3.js} +1 -1
- package/dist/assets/{file-icons-uUY_hGYm.js → file-icons-Dx3v6lMu.js} +1 -1
- package/dist/assets/{file-name-input-DWUKg4LS.js → file-name-input-DzL8D_7x.js} +1 -1
- package/dist/assets/{floating-outline-DhGtHg6T.js → floating-outline-3rQEbxxk.js} +1 -1
- package/dist/assets/{home-page-Bka_7H02.js → home-page-Dnz4tfZB.js} +1 -1
- package/dist/assets/index-BID9w87A.js +29 -0
- package/dist/assets/layout-qHVpnnQT.js +9 -0
- package/dist/assets/{logs-panel-ragZa1sf.js → logs-panel-CSbUhhRo.js} +1 -1
- package/dist/assets/{markdown-renderer-B-uA_6mP.js → markdown-renderer-BgUTGtM0.js} +1 -1
- package/dist/assets/{name-cell-input-gC1flbyT.js → name-cell-input-pEE4FGAL.js} +1 -1
- package/dist/assets/{outline-panel-D7H7bhOh.js → outline-panel-BgnjEQWg.js} +1 -1
- package/dist/assets/{packages-panel-BZnsUm4S.js → packages-panel-BprfUOzZ.js} +1 -1
- package/dist/assets/panel-accordion-state-vpqEc3Kd.js +1 -0
- package/dist/assets/panels-3h7Cf8xJ.js +4 -0
- package/dist/assets/{process-output-DQQ8T5gv.js → process-output-siRm_lza.js} +1 -1
- package/dist/assets/{readonly-code-display-Bc0VX9i6.js → readonly-code-display-CLWV76FT.js} +1 -1
- package/dist/assets/readonly-python-code-Bor9b9pV.js +1 -0
- package/dist/assets/reveal-component-DNPksvzT.js +1069 -0
- package/dist/assets/run-page-C4aONLoP.js +1 -0
- package/dist/assets/{scratchpad-panel-BJHlMBZV.js → scratchpad-panel-BQSoRQvQ.js} +1 -1
- package/dist/assets/session-panel-DB5qEYqt.js +1 -0
- package/dist/assets/skeleton-BSxwEE0h.js +1 -0
- package/dist/assets/{snippets-panel-2jq36DdG.js → snippets-panel-B8Yi81tz.js} +1 -1
- package/dist/assets/state-D1N9_41P.js +3 -0
- package/dist/assets/state-O8w98G11.js +1 -0
- package/dist/assets/{tracing-91_akEkv.js → tracing-DuLkr7ts.js} +1 -1
- package/dist/assets/{tracing-panel-BvE81RYx.js → tracing-panel-CeLKVG8M.js} +2 -2
- package/dist/assets/{tree-actions-CGkbUSfG.js → tree-actions-Bh-Nnaub.js} +1 -1
- package/dist/assets/useAddCell-_JRYhAFd.js +1 -0
- package/dist/assets/useBoolean-Cu7Eado1.js +1 -0
- package/dist/assets/{useCellActionButton-cv6xOsnc.js → useCellActionButton-Bi8Lnv3F.js} +1 -1
- package/dist/assets/{useDeleteCell-DB-bhhnO.js → useDeleteCell-CBQiavzs.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-BDnVzNV-.js → useDependencyPanelTab-BOn3UJ07.js} +1 -1
- package/dist/assets/useNotebookActions--pblpIWF.js +10 -0
- package/dist/assets/{useRunCells-GCTS1YFv.js → useRunCells-BFQfLlUl.js} +1 -1
- package/dist/assets/{useSplitCell-DhT9BA2_.js → useSplitCell-Ig9P6lAn.js} +1 -1
- package/dist/index.html +20 -20
- package/package.json +1 -1
- package/src/__tests__/mount.test.ts +54 -2
- package/src/components/editor/__tests__/viewer-banner.test.tsx +1 -1
- package/src/components/editor/actions/export-dialog/__tests__/export-notebook.test.ts +9 -0
- package/src/components/editor/actions/export-dialog/export-notebook.ts +8 -1
- package/src/components/editor/actions/export-dialog/use-export-dialog.ts +2 -0
- package/src/components/editor/actions/useNotebookActions.tsx +1 -1
- package/src/components/editor/controls/__tests__/notebook-menu-dropdown.test.tsx +1 -1
- package/src/components/editor/renderers/__tests__/cells-renderer.test.tsx +1 -1
- package/src/components/editor/renderers/cells-renderer.tsx +15 -28
- package/src/components/editor/renderers/grid-layout/__tests__/plugin.test.ts +42 -0
- package/src/components/editor/renderers/grid-layout/plugin.tsx +20 -10
- package/src/components/editor/renderers/layout-select.tsx +1 -1
- package/src/components/editor/renderers/plugins.ts +7 -1
- package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +49 -15
- package/src/components/editor/renderers/slides-layout/slides-layout.tsx +16 -2
- package/src/components/editor/renderers/slides-layout/types.ts +15 -11
- package/src/components/editor/renderers/types.ts +5 -5
- package/src/components/editor/viewer-banner.tsx +1 -1
- package/src/components/pages/run-page.tsx +16 -7
- package/src/components/slides/__tests__/reveal-component.test.ts +33 -8
- package/src/components/slides/__tests__/speaker-view.test.ts +27 -0
- package/src/components/slides/reveal-component.tsx +110 -45
- package/src/components/slides/speaker-view.ts +22 -0
- package/src/components/static-html/share-modal.tsx +2 -0
- package/src/core/cells/__tests__/document-changes.test.ts +343 -26
- package/src/core/cells/document-changes.ts +181 -4
- package/src/core/export/hooks.ts +2 -0
- package/src/core/export/layout.ts +9 -0
- package/src/core/kernel/handlers.ts +7 -16
- package/src/core/kernel/state.ts +2 -0
- package/src/core/layout/__tests__/layout.test.ts +95 -0
- package/src/core/layout/layout.ts +59 -65
- package/src/core/layout/state.ts +121 -0
- package/src/core/meta/__tests__/code-visibility.test.tsx +28 -0
- package/src/core/meta/code-visibility.ts +3 -1
- package/src/core/run-app.tsx +6 -2
- package/src/core/saving/__tests__/save-component.test.ts +31 -1
- package/src/core/saving/save-component.tsx +39 -29
- package/src/core/saving/state.ts +1 -1
- package/src/core/static/download-html.ts +2 -0
- package/src/core/websocket/__tests__/useMarimoKernelConnection.hook.test.tsx +47 -14
- package/src/core/websocket/useMarimoKernelConnection.tsx +7 -2
- package/src/mount.tsx +24 -6
- package/dist/assets/ai-model-dropdown-CfWMIzjl.js +0 -3
- package/dist/assets/app-config-button-ii-Hj5CZ.js +0 -1
- package/dist/assets/cell-editor-BVb6iKbw.js +0 -21
- package/dist/assets/edit-page-RuyN164g.js +0 -9
- package/dist/assets/index-ChbtcNW2.js +0 -29
- package/dist/assets/layout-Uv1ZP5XB.js +0 -9
- package/dist/assets/panel-accordion-state-BLU7OiMD.js +0 -1
- package/dist/assets/panels-B3Y6fue9.js +0 -4
- package/dist/assets/readonly-python-code-D2HXwAfT.js +0 -1
- package/dist/assets/reveal-component-BN1C8zFu.js +0 -1069
- package/dist/assets/run-page-ByVvsH9G.js +0 -1
- package/dist/assets/session-panel-CdCKWAOf.js +0 -1
- package/dist/assets/skeleton-DirOIQrj.js +0 -1
- package/dist/assets/state-C9O-2hQL.js +0 -3
- package/dist/assets/state-DyxPdcEa.js +0 -1
- package/dist/assets/types-CoHi_MQq.js +0 -1
- package/dist/assets/useAddCell-DqvY2ulx.js +0 -1
- package/dist/assets/useBoolean-DWBCpKfT.js +0 -1
- package/dist/assets/useNotebookActions-Dxp4gTEa.js +0 -10
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
import { deserializeLayout } from "@/components/editor/renderers/plugins";
|
|
3
2
|
import type { LayoutType } from "@/components/editor/renderers/types";
|
|
4
3
|
import { Logger } from "@/utils/Logger";
|
|
5
4
|
import { Objects } from "@/utils/objects";
|
|
@@ -7,11 +6,8 @@ import type { UIElementId } from "../cells/ids";
|
|
|
7
6
|
import { type CellData, createCell } from "../cells/types";
|
|
8
7
|
import { type AppConfig, AppConfigSchema } from "../config/config-schema";
|
|
9
8
|
import { UI_ELEMENT_REGISTRY } from "../dom/uiregistry";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
type LayoutData,
|
|
13
|
-
type LayoutState,
|
|
14
|
-
} from "../layout/layout";
|
|
9
|
+
import { deserializeLayoutState } from "../layout/layout";
|
|
10
|
+
import type { LayoutData, LayoutState } from "../layout/state";
|
|
15
11
|
import { getRequestClient } from "../network/requests";
|
|
16
12
|
import { VirtualFileTracker } from "../static/virtual-file-tracker";
|
|
17
13
|
import type {
|
|
@@ -69,19 +65,14 @@ export function buildLayoutState(
|
|
|
69
65
|
data: LayoutData;
|
|
70
66
|
}) => void,
|
|
71
67
|
): LayoutState {
|
|
72
|
-
const layoutState = initialLayoutState();
|
|
73
68
|
const { layout } = data;
|
|
69
|
+
const layoutState = deserializeLayoutState(layout, cells);
|
|
74
70
|
|
|
75
|
-
if (layout) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
data: layout.data,
|
|
80
|
-
cells,
|
|
71
|
+
if (layout && layout.type === layoutState.selectedLayout) {
|
|
72
|
+
setLayoutData({
|
|
73
|
+
layoutView: layoutState.selectedLayout,
|
|
74
|
+
data: layoutState.layoutData[layoutState.selectedLayout],
|
|
81
75
|
});
|
|
82
|
-
layoutState.selectedLayout = layoutType;
|
|
83
|
-
layoutState.layoutData[layoutType] = layoutData;
|
|
84
|
-
setLayoutData({ layoutView: layoutType, data: layoutData });
|
|
85
76
|
}
|
|
86
77
|
|
|
87
78
|
return layoutState;
|
package/src/core/kernel/state.ts
CHANGED
|
@@ -12,6 +12,8 @@ export const kernelStateAtom = atom<KernelState>({
|
|
|
12
12
|
error: null,
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
+
export const initialRunCompletedAtom = atom(false);
|
|
16
|
+
|
|
15
17
|
export function waitForKernelToBeInstantiated(): Promise<KernelState> {
|
|
16
18
|
return waitFor(kernelStateAtom, (value) => value.isInstantiated);
|
|
17
19
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
4
|
+
import { MockNotebook } from "@/__mocks__/notebook";
|
|
5
|
+
import { cellId } from "@/__tests__/branded";
|
|
6
|
+
import { notebookAtom } from "@/core/cells/cells";
|
|
7
|
+
import { store } from "@/core/state/jotai";
|
|
8
|
+
import { getSerializedLayout } from "../layout";
|
|
9
|
+
import {
|
|
10
|
+
initialLayoutState,
|
|
11
|
+
layoutStateAtom,
|
|
12
|
+
resolveLayoutType,
|
|
13
|
+
selectLayout,
|
|
14
|
+
} from "../state";
|
|
15
|
+
|
|
16
|
+
describe("layout state", () => {
|
|
17
|
+
it("falls back to vertical for an unknown layout type", () => {
|
|
18
|
+
expect(initialLayoutState({ type: "unknown", data: {} })).toEqual({
|
|
19
|
+
selectedLayout: "vertical",
|
|
20
|
+
layoutData: {},
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("preserves pending data until a different layout is selected", () => {
|
|
25
|
+
const state = initialLayoutState({
|
|
26
|
+
type: "slides",
|
|
27
|
+
data: { deck: { transition: "fade" } },
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(selectLayout(state, "slides")).toBe(state);
|
|
31
|
+
expect(selectLayout(state, "grid")).toEqual({
|
|
32
|
+
selectedLayout: "grid",
|
|
33
|
+
layoutData: {},
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it.each([
|
|
38
|
+
{ isReading: true, expected: "slides" },
|
|
39
|
+
{ isReading: false, expected: "vertical" },
|
|
40
|
+
] as const)(
|
|
41
|
+
"returns $expected when read mode is $isReading",
|
|
42
|
+
({ isReading, expected }) => {
|
|
43
|
+
expect(
|
|
44
|
+
resolveLayoutType({
|
|
45
|
+
selectedLayout: "vertical",
|
|
46
|
+
isReading,
|
|
47
|
+
searchParams: new URLSearchParams("view-as=slides"),
|
|
48
|
+
}),
|
|
49
|
+
).toBe(expected);
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("getSerializedLayout", () => {
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
store.set(layoutStateAtom, initialLayoutState());
|
|
57
|
+
store.set(
|
|
58
|
+
notebookAtom,
|
|
59
|
+
MockNotebook.notebookState({
|
|
60
|
+
cellData: {
|
|
61
|
+
[cellId("cell-1")]: { code: "first = 1" },
|
|
62
|
+
[cellId("cell-2")]: { code: "second = 2" },
|
|
63
|
+
},
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("preserves a pending embedded layout when edit mode saves", () => {
|
|
69
|
+
const layout = {
|
|
70
|
+
type: "slides",
|
|
71
|
+
data: {
|
|
72
|
+
cells: [
|
|
73
|
+
{ type: "slide", speakerNotes: "Introduce the result" },
|
|
74
|
+
{ type: "fragment" },
|
|
75
|
+
],
|
|
76
|
+
deck: { transition: "fade", verticalAlign: "center" },
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
store.set(layoutStateAtom, initialLayoutState(layout));
|
|
80
|
+
|
|
81
|
+
expect(getSerializedLayout()).toEqual(layout);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("uses the initial layout when materialized data is null", () => {
|
|
85
|
+
store.set(layoutStateAtom, {
|
|
86
|
+
selectedLayout: "slides",
|
|
87
|
+
layoutData: { slides: null },
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(getSerializedLayout()).toEqual({
|
|
91
|
+
type: "slides",
|
|
92
|
+
data: { cells: [{}, {}], deck: {} },
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -1,89 +1,86 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
cellRendererPlugins,
|
|
5
|
+
deserializeLayout,
|
|
6
|
+
} from "@/components/editor/renderers/plugins";
|
|
7
|
+
import type { ICellRendererPlugin } from "@/components/editor/renderers/types";
|
|
8
8
|
import { Logger } from "@/utils/Logger";
|
|
9
9
|
import { getNotebook } from "../cells/cells";
|
|
10
|
+
import type { CellData } from "../cells/types";
|
|
10
11
|
import { notebookCells } from "../cells/utils";
|
|
11
12
|
import { store } from "../state/jotai";
|
|
13
|
+
import {
|
|
14
|
+
initialLayoutState,
|
|
15
|
+
layoutStateAtom,
|
|
16
|
+
type LayoutState,
|
|
17
|
+
type SerializedLayout,
|
|
18
|
+
} from "./state";
|
|
12
19
|
|
|
13
|
-
export
|
|
20
|
+
export function deserializeLayoutState(
|
|
21
|
+
layout: SerializedLayout | null | undefined,
|
|
22
|
+
cells: CellData[],
|
|
23
|
+
): LayoutState {
|
|
24
|
+
if (layout == null) {
|
|
25
|
+
return initialLayoutState();
|
|
26
|
+
}
|
|
14
27
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
28
|
+
const plugin = cellRendererPlugins.find(
|
|
29
|
+
(candidate) => candidate.type === layout.type,
|
|
30
|
+
);
|
|
31
|
+
if (plugin === undefined) {
|
|
32
|
+
Logger.warn(`Unknown layout type: ${layout.type}`);
|
|
33
|
+
return initialLayoutState();
|
|
34
|
+
}
|
|
19
35
|
|
|
20
|
-
export function initialLayoutState(): LayoutState {
|
|
21
36
|
return {
|
|
22
|
-
selectedLayout:
|
|
23
|
-
layoutData: {
|
|
37
|
+
selectedLayout: plugin.type,
|
|
38
|
+
layoutData: {
|
|
39
|
+
[plugin.type]: deserializeLayout({
|
|
40
|
+
type: plugin.type,
|
|
41
|
+
data: layout.data,
|
|
42
|
+
cells,
|
|
43
|
+
}),
|
|
44
|
+
},
|
|
24
45
|
};
|
|
25
46
|
}
|
|
26
47
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
setCurrentLayoutData: (state, payload: LayoutData) => {
|
|
50
|
-
return {
|
|
51
|
-
...state,
|
|
52
|
-
layoutData: {
|
|
53
|
-
...state.layoutData,
|
|
54
|
-
[state.selectedLayout]: payload,
|
|
55
|
-
},
|
|
56
|
-
};
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
export { layoutStateAtom };
|
|
62
|
-
|
|
63
|
-
export const useLayoutState = () => {
|
|
64
|
-
return useAtomValue(layoutStateAtom);
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
export const useLayoutActions = () => {
|
|
68
|
-
return useActions();
|
|
69
|
-
};
|
|
48
|
+
export function resolveLayoutData<S, L>({
|
|
49
|
+
state,
|
|
50
|
+
plugin,
|
|
51
|
+
cells,
|
|
52
|
+
}: {
|
|
53
|
+
state: LayoutState;
|
|
54
|
+
plugin: ICellRendererPlugin<S, L>;
|
|
55
|
+
cells: CellData[];
|
|
56
|
+
}): L {
|
|
57
|
+
const materialized = state.layoutData[plugin.type];
|
|
58
|
+
if (materialized != null) {
|
|
59
|
+
return materialized as L;
|
|
60
|
+
}
|
|
61
|
+
if (state.pendingLayout?.type === plugin.type) {
|
|
62
|
+
return deserializeLayout({
|
|
63
|
+
type: plugin.type,
|
|
64
|
+
data: state.pendingLayout.data,
|
|
65
|
+
cells,
|
|
66
|
+
}) as L;
|
|
67
|
+
}
|
|
68
|
+
return plugin.getInitialLayout(cells);
|
|
69
|
+
}
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
72
|
* Get the serialized layout data, to be used when saving.
|
|
73
73
|
*/
|
|
74
74
|
export function getSerializedLayout() {
|
|
75
75
|
const notebook = getNotebook();
|
|
76
|
-
const
|
|
76
|
+
const layoutState = store.get(layoutStateAtom);
|
|
77
|
+
const { selectedLayout } = layoutState;
|
|
77
78
|
|
|
78
79
|
// Vertical layout has no data, as it is the default.
|
|
79
80
|
if (selectedLayout === "vertical") {
|
|
80
81
|
return null;
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
if (layoutData === undefined) {
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
84
|
const plugin = cellRendererPlugins.find(
|
|
88
85
|
(plugin) => plugin.type === selectedLayout,
|
|
89
86
|
);
|
|
@@ -92,10 +89,7 @@ export function getSerializedLayout() {
|
|
|
92
89
|
return null;
|
|
93
90
|
}
|
|
94
91
|
const cells = notebookCells(notebook);
|
|
95
|
-
|
|
96
|
-
// interacted with this layout — otherwise serializers that expect a
|
|
97
|
-
// structured layout object crash on `undefined`.
|
|
98
|
-
const data = layoutData[selectedLayout] ?? plugin.getInitialLayout(cells);
|
|
92
|
+
const data = resolveLayoutData({ state: layoutState, plugin, cells });
|
|
99
93
|
return {
|
|
100
94
|
type: selectedLayout,
|
|
101
95
|
data: plugin.serializeLayout(data, cells),
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { useAtomValue } from "jotai";
|
|
4
|
+
import type { GridLayout } from "@/components/editor/renderers/grid-layout/types";
|
|
5
|
+
import type { SlidesLayout } from "@/components/editor/renderers/slides-layout/types";
|
|
6
|
+
import {
|
|
7
|
+
type LayoutType,
|
|
8
|
+
isLayoutType,
|
|
9
|
+
OVERRIDABLE_LAYOUT_TYPES,
|
|
10
|
+
} from "@/components/editor/renderers/types";
|
|
11
|
+
import { KnownQueryParams } from "@/core/constants";
|
|
12
|
+
import { createReducerAndAtoms } from "@/utils/createReducer";
|
|
13
|
+
import { isRecord } from "@/utils/records";
|
|
14
|
+
|
|
15
|
+
export type LayoutData = GridLayout | SlidesLayout | null | undefined;
|
|
16
|
+
|
|
17
|
+
export interface SerializedLayout {
|
|
18
|
+
type: string;
|
|
19
|
+
data: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isSerializedLayout(value: unknown): value is SerializedLayout {
|
|
23
|
+
return isRecord(value) && typeof value.type === "string" && "data" in value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface LayoutStateBase {
|
|
27
|
+
selectedLayout: LayoutType;
|
|
28
|
+
layoutData: Partial<Record<LayoutType, LayoutData>>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type LayoutState =
|
|
32
|
+
| (LayoutStateBase & { pendingLayout: SerializedLayout })
|
|
33
|
+
| (LayoutStateBase & { pendingLayout?: never });
|
|
34
|
+
|
|
35
|
+
export function initialLayoutState(
|
|
36
|
+
serializedLayout?: SerializedLayout,
|
|
37
|
+
): LayoutState {
|
|
38
|
+
if (serializedLayout && isLayoutType(serializedLayout.type)) {
|
|
39
|
+
return {
|
|
40
|
+
selectedLayout: serializedLayout.type,
|
|
41
|
+
layoutData: {},
|
|
42
|
+
pendingLayout: serializedLayout,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
selectedLayout: "vertical",
|
|
47
|
+
layoutData: {},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function selectLayout(
|
|
52
|
+
state: LayoutState,
|
|
53
|
+
selectedLayout: LayoutType,
|
|
54
|
+
): LayoutState {
|
|
55
|
+
if (selectedLayout === state.selectedLayout) {
|
|
56
|
+
return state;
|
|
57
|
+
}
|
|
58
|
+
const { pendingLayout: _pendingLayout, ...materializedState } = state;
|
|
59
|
+
return { ...materializedState, selectedLayout };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const { valueAtom: layoutStateAtom, useActions } = createReducerAndAtoms(
|
|
63
|
+
initialLayoutState,
|
|
64
|
+
{
|
|
65
|
+
setLayoutView: selectLayout,
|
|
66
|
+
setLayoutData: (
|
|
67
|
+
{ pendingLayout: _pendingLayout, ...state },
|
|
68
|
+
payload: { layoutView: LayoutType; data: LayoutData },
|
|
69
|
+
) => {
|
|
70
|
+
return {
|
|
71
|
+
...state,
|
|
72
|
+
selectedLayout: payload.layoutView,
|
|
73
|
+
layoutData: {
|
|
74
|
+
...state.layoutData,
|
|
75
|
+
[payload.layoutView]: payload.data,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
setCurrentLayoutData: (
|
|
80
|
+
{ pendingLayout: _pendingLayout, ...state },
|
|
81
|
+
payload: LayoutData,
|
|
82
|
+
) => {
|
|
83
|
+
return {
|
|
84
|
+
...state,
|
|
85
|
+
layoutData: {
|
|
86
|
+
...state.layoutData,
|
|
87
|
+
[state.selectedLayout]: payload,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
export { layoutStateAtom };
|
|
95
|
+
|
|
96
|
+
export const useLayoutState = () => {
|
|
97
|
+
return useAtomValue(layoutStateAtom);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const useLayoutActions = () => {
|
|
101
|
+
return useActions();
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export function resolveLayoutType({
|
|
105
|
+
selectedLayout,
|
|
106
|
+
isReading,
|
|
107
|
+
searchParams,
|
|
108
|
+
}: {
|
|
109
|
+
selectedLayout: LayoutType;
|
|
110
|
+
isReading: boolean;
|
|
111
|
+
searchParams: URLSearchParams;
|
|
112
|
+
}): LayoutType {
|
|
113
|
+
if (!isReading) {
|
|
114
|
+
return selectedLayout;
|
|
115
|
+
}
|
|
116
|
+
const requested = searchParams.get(KnownQueryParams.viewAs);
|
|
117
|
+
return (
|
|
118
|
+
OVERRIDABLE_LAYOUT_TYPES.find((layout) => layout === requested) ??
|
|
119
|
+
selectedLayout
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -6,23 +6,32 @@ import type { ReactNode } from "react";
|
|
|
6
6
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
7
7
|
import { showCodeInRunModeAtom } from "@/core/meta/state";
|
|
8
8
|
import { type AppMode, kioskModeAtom, viewStateAtom } from "@/core/mode";
|
|
9
|
+
import {
|
|
10
|
+
remoteAdapter,
|
|
11
|
+
runtimeAdapterAtom,
|
|
12
|
+
staticAdapter,
|
|
13
|
+
type RuntimeAdapter,
|
|
14
|
+
} from "@/core/runtime/adapter";
|
|
9
15
|
import { useNotebookCodeAvailable } from "../code-visibility";
|
|
10
16
|
|
|
11
17
|
interface StoreOpts {
|
|
12
18
|
mode?: AppMode;
|
|
13
19
|
kiosk?: boolean;
|
|
14
20
|
showInRunMode?: boolean;
|
|
21
|
+
runtime?: RuntimeAdapter;
|
|
15
22
|
}
|
|
16
23
|
|
|
17
24
|
function makeStore({
|
|
18
25
|
mode = "read",
|
|
19
26
|
kiosk = false,
|
|
20
27
|
showInRunMode = true,
|
|
28
|
+
runtime = remoteAdapter,
|
|
21
29
|
}: StoreOpts = {}) {
|
|
22
30
|
const store = createStore();
|
|
23
31
|
store.set(viewStateAtom, { mode, cellAnchor: null });
|
|
24
32
|
store.set(kioskModeAtom, kiosk);
|
|
25
33
|
store.set(showCodeInRunModeAtom, showInRunMode);
|
|
34
|
+
store.set(runtimeAdapterAtom, runtime);
|
|
26
35
|
return store;
|
|
27
36
|
}
|
|
28
37
|
|
|
@@ -128,6 +137,25 @@ describe("useNotebookCodeAvailable", () => {
|
|
|
128
137
|
expect(result.current).toBe(true);
|
|
129
138
|
});
|
|
130
139
|
|
|
140
|
+
it("checks embedded code in a static kiosk", () => {
|
|
141
|
+
const store = makeStore({
|
|
142
|
+
mode: "read",
|
|
143
|
+
kiosk: true,
|
|
144
|
+
runtime: staticAdapter,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const withoutCode = renderHook(
|
|
148
|
+
() => useNotebookCodeAvailable(cellsWithoutCode),
|
|
149
|
+
{ wrapper: wrap(store) },
|
|
150
|
+
);
|
|
151
|
+
const withCode = renderHook(() => useNotebookCodeAvailable(cellsWithCode), {
|
|
152
|
+
wrapper: wrap(store),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(withoutCode.result.current).toBe(false);
|
|
156
|
+
expect(withCode.result.current).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
131
159
|
it("returns false for non-edit/non-read modes (home, gallery)", () => {
|
|
132
160
|
for (const mode of ["home", "gallery"] as const) {
|
|
133
161
|
const store = makeStore({ mode });
|
|
@@ -4,6 +4,7 @@ import { useAtomValue } from "jotai";
|
|
|
4
4
|
import { KnownQueryParams } from "@/core/constants";
|
|
5
5
|
import { showCodeInRunModeAtom } from "@/core/meta/state";
|
|
6
6
|
import { kioskModeAtom, viewStateAtom } from "@/core/mode";
|
|
7
|
+
import { runtimeAdapterAtom } from "@/core/runtime/adapter";
|
|
7
8
|
import { logNever } from "@/utils/assertNever";
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -19,8 +20,9 @@ export function useNotebookCodeAvailable(
|
|
|
19
20
|
const kioskMode = useAtomValue(kioskModeAtom);
|
|
20
21
|
const { mode } = useAtomValue(viewStateAtom);
|
|
21
22
|
const showInRunMode = useAtomValue(showCodeInRunModeAtom);
|
|
23
|
+
const runtimeKind = useAtomValue(runtimeAdapterAtom).kind;
|
|
22
24
|
|
|
23
|
-
if (kioskMode) {
|
|
25
|
+
if (kioskMode && runtimeKind === "remote") {
|
|
24
26
|
return true;
|
|
25
27
|
}
|
|
26
28
|
|
package/src/core/run-app.tsx
CHANGED
|
@@ -25,6 +25,7 @@ import { useMarimoKernelConnection } from "./websocket/useMarimoKernelConnection
|
|
|
25
25
|
|
|
26
26
|
interface AppProps {
|
|
27
27
|
appConfig: AppConfig;
|
|
28
|
+
hideHeader?: boolean;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
/**
|
|
@@ -37,7 +38,7 @@ const canPaintRunAppAtom = atom(
|
|
|
37
38
|
(get) => get(hasCellsAtom) || !isAppConnecting(get(connectionAtom).state),
|
|
38
39
|
);
|
|
39
40
|
|
|
40
|
-
export const RunApp: React.FC<AppProps> = ({ appConfig }) => {
|
|
41
|
+
export const RunApp: React.FC<AppProps> = ({ appConfig, hideHeader }) => {
|
|
41
42
|
const { setCells } = useCellActions();
|
|
42
43
|
const { sendComponentValues } = useRequestClient();
|
|
43
44
|
|
|
@@ -77,7 +78,10 @@ export const RunApp: React.FC<AppProps> = ({ appConfig }) => {
|
|
|
77
78
|
width={appConfig.width}
|
|
78
79
|
onReconnect={reconnect}
|
|
79
80
|
>
|
|
80
|
-
<AppHeader
|
|
81
|
+
<AppHeader
|
|
82
|
+
connection={connection}
|
|
83
|
+
className={hideHeader ? "hidden" : "sm:pt-8"}
|
|
84
|
+
>
|
|
81
85
|
{galleryHref && (
|
|
82
86
|
<div className="flex items-center px-6 pt-4 sm:-mt-8">
|
|
83
87
|
<a
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { describe, expect, it } from "vitest";
|
|
4
|
-
import {
|
|
4
|
+
import { Deferred } from "@/utils/Deferred";
|
|
5
|
+
import { enqueueNotebookSave, isNamedPersistentFile } from "../save-component";
|
|
5
6
|
|
|
6
7
|
describe("isNamedPersistentFile", () => {
|
|
7
8
|
it.each([
|
|
@@ -18,3 +19,32 @@ describe("isNamedPersistentFile", () => {
|
|
|
18
19
|
expect(isNamedPersistentFile(filename)).toBe(expected);
|
|
19
20
|
});
|
|
20
21
|
});
|
|
22
|
+
|
|
23
|
+
describe("enqueueNotebookSave", () => {
|
|
24
|
+
it("serializes snapshots across callers and continues after a failure", async () => {
|
|
25
|
+
const firstStarted = new Deferred<void>();
|
|
26
|
+
const releaseFirst = new Deferred<void>();
|
|
27
|
+
const snapshots: string[] = [];
|
|
28
|
+
let current = "first";
|
|
29
|
+
|
|
30
|
+
const first = enqueueNotebookSave(async () => {
|
|
31
|
+
snapshots.push(current);
|
|
32
|
+
firstStarted.resolve();
|
|
33
|
+
await releaseFirst.promise;
|
|
34
|
+
throw new Error("save failed");
|
|
35
|
+
});
|
|
36
|
+
await firstStarted.promise;
|
|
37
|
+
|
|
38
|
+
current = "queued";
|
|
39
|
+
const second = enqueueNotebookSave(async () => {
|
|
40
|
+
snapshots.push(current);
|
|
41
|
+
});
|
|
42
|
+
current = "latest";
|
|
43
|
+
|
|
44
|
+
expect(snapshots).toEqual(["first"]);
|
|
45
|
+
releaseFirst.resolve();
|
|
46
|
+
await expect(first).rejects.toThrow("save failed");
|
|
47
|
+
await second;
|
|
48
|
+
expect(snapshots).toEqual(["first", "latest"]);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -21,11 +21,13 @@ import { Label } from "../../components/ui/label";
|
|
|
21
21
|
import { useEvent } from "../../hooks/useEvent";
|
|
22
22
|
import { Logger } from "../../utils/Logger";
|
|
23
23
|
import { getCellConfigs, getNotebook, useNotebook } from "../cells/cells";
|
|
24
|
+
import { withDocumentSave } from "../cells/document-changes";
|
|
24
25
|
import { notebookCells } from "../cells/utils";
|
|
25
26
|
import { formatAll } from "../codemirror/format";
|
|
26
27
|
import { autoSaveConfigAtom } from "../config/config";
|
|
27
28
|
import { useAutoExport } from "../export/hooks";
|
|
28
|
-
import { getSerializedLayout
|
|
29
|
+
import { getSerializedLayout } from "../layout/layout";
|
|
30
|
+
import { layoutStateAtom } from "../layout/state";
|
|
29
31
|
import { kioskModeAtom } from "../mode";
|
|
30
32
|
import { connectionAtom } from "../network/connection";
|
|
31
33
|
import { useRequestClient } from "../network/requests";
|
|
@@ -39,6 +41,17 @@ interface SaveNotebookProps {
|
|
|
39
41
|
kioskMode: boolean;
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
let notebookSaveQueue: Promise<void> = Promise.resolve();
|
|
45
|
+
|
|
46
|
+
export function enqueueNotebookSave<T>(save: () => Promise<T>): Promise<T> {
|
|
47
|
+
const result = notebookSaveQueue.then(save);
|
|
48
|
+
notebookSaveQueue = result.then(
|
|
49
|
+
() => undefined,
|
|
50
|
+
() => undefined,
|
|
51
|
+
);
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
42
55
|
export const SaveComponent = ({ kioskMode }: SaveNotebookProps) => {
|
|
43
56
|
const filename = useFilename();
|
|
44
57
|
const needsSave = useAtomValue(needsSaveAtom);
|
|
@@ -96,8 +109,8 @@ export function useSaveNotebook() {
|
|
|
96
109
|
const store = useStore();
|
|
97
110
|
|
|
98
111
|
// Save the notebook with the given filename
|
|
99
|
-
const saveNotebook = useEvent(
|
|
100
|
-
async (
|
|
112
|
+
const saveNotebook = useEvent((filename: string, userInitiated: boolean) =>
|
|
113
|
+
enqueueNotebookSave(async () => {
|
|
101
114
|
const connection = store.get(connectionAtom);
|
|
102
115
|
const autoSaveConfig = store.get(autoSaveConfigAtom);
|
|
103
116
|
const kioskMode = store.get(kioskModeAtom);
|
|
@@ -119,37 +132,34 @@ export function useSaveNotebook() {
|
|
|
119
132
|
await formatAll();
|
|
120
133
|
}
|
|
121
134
|
|
|
122
|
-
|
|
123
|
-
const notebook = getNotebook();
|
|
124
|
-
const cells = notebookCells(notebook);
|
|
125
|
-
const cellIds = cells.map((cell) => cell.id);
|
|
126
|
-
const codes = cells.map((cell) => cell.code);
|
|
127
|
-
const cellNames = cells.map((cell) => cell.name);
|
|
128
|
-
const configs = getCellConfigs(notebook);
|
|
129
|
-
const layout = store.get(layoutStateAtom);
|
|
130
|
-
|
|
131
|
-
// Don't save if there are no cells
|
|
132
|
-
if (codes.length === 0) {
|
|
135
|
+
if (notebookCells(getNotebook()).length === 0) {
|
|
133
136
|
return;
|
|
134
137
|
}
|
|
135
138
|
|
|
136
|
-
await
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
139
|
+
const savedNotebook = await withDocumentSave(async () => {
|
|
140
|
+
// Grab the latest notebook state after formatting and after every
|
|
141
|
+
// earlier document transaction has reached the server.
|
|
142
|
+
const notebook = getNotebook();
|
|
143
|
+
const cells = notebookCells(notebook);
|
|
144
|
+
const cellIds = cells.map((cell) => cell.id);
|
|
145
|
+
const codes = cells.map((cell) => cell.code);
|
|
146
|
+
const cellNames = cells.map((cell) => cell.name);
|
|
147
|
+
const configs = getCellConfigs(notebook);
|
|
148
|
+
const layout = store.get(layoutStateAtom);
|
|
145
149
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
150
|
+
await sendSave({
|
|
151
|
+
cellIds: cellIds,
|
|
152
|
+
codes,
|
|
153
|
+
names: cellNames,
|
|
154
|
+
filename,
|
|
155
|
+
configs,
|
|
156
|
+
layout: getSerializedLayout(),
|
|
157
|
+
persist: true,
|
|
158
|
+
});
|
|
159
|
+
return { names: cellNames, codes, configs, layout };
|
|
151
160
|
});
|
|
152
|
-
|
|
161
|
+
setLastSavedNotebook(savedNotebook);
|
|
162
|
+
}),
|
|
153
163
|
);
|
|
154
164
|
|
|
155
165
|
// Save the notebook with the current filename, only if the filename exists
|