@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
|
@@ -5,19 +5,15 @@ import type React from "react";
|
|
|
5
5
|
import { memo, type PropsWithChildren } from "react";
|
|
6
6
|
import { flattenTopLevelNotebookCells, useNotebook } from "@/core/cells/cells";
|
|
7
7
|
import type { AppConfig } from "@/core/config/config-schema";
|
|
8
|
-
import {
|
|
8
|
+
import { resolveLayoutData } from "@/core/layout/layout";
|
|
9
9
|
import {
|
|
10
|
-
|
|
10
|
+
resolveLayoutType,
|
|
11
11
|
useLayoutActions,
|
|
12
12
|
useLayoutState,
|
|
13
|
-
} from "@/core/layout/
|
|
13
|
+
} from "@/core/layout/state";
|
|
14
14
|
import { type AppMode, kioskModeAtom } from "@/core/mode";
|
|
15
15
|
import { cellRendererPlugins } from "./plugins";
|
|
16
|
-
import {
|
|
17
|
-
type ICellRendererPlugin,
|
|
18
|
-
type LayoutType,
|
|
19
|
-
OVERRIDABLE_LAYOUT_TYPES,
|
|
20
|
-
} from "./types";
|
|
16
|
+
import type { ICellRendererPlugin } from "./types";
|
|
21
17
|
|
|
22
18
|
interface Props {
|
|
23
19
|
appConfig: AppConfig;
|
|
@@ -26,7 +22,7 @@ interface Props {
|
|
|
26
22
|
|
|
27
23
|
export const CellsRenderer: React.FC<PropsWithChildren<Props>> = memo(
|
|
28
24
|
({ appConfig, mode, children }) => {
|
|
29
|
-
const { selectedLayout
|
|
25
|
+
const { selectedLayout } = useLayoutState();
|
|
30
26
|
const kioskMode = useAtomValue(kioskModeAtom);
|
|
31
27
|
|
|
32
28
|
// Render children (the editable notebook) in edit mode, and in present
|
|
@@ -43,14 +39,11 @@ export const CellsRenderer: React.FC<PropsWithChildren<Props>> = memo(
|
|
|
43
39
|
// We allow overriding the layout type by url params when in 'read' mode,
|
|
44
40
|
// for example, forcing the 'slides' view.
|
|
45
41
|
// https://marimo.app/?slug=14ovyr8&mode=run&view-as=slides
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
finalLayout = viewAsOverride as LayoutType;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
42
|
+
const finalLayout = resolveLayoutType({
|
|
43
|
+
selectedLayout,
|
|
44
|
+
isReading: mode === "read",
|
|
45
|
+
searchParams: new URLSearchParams(window.location.search),
|
|
46
|
+
});
|
|
54
47
|
|
|
55
48
|
const plugin = cellRendererPlugins.find((p) => p.type === finalLayout);
|
|
56
49
|
|
|
@@ -60,13 +53,7 @@ export const CellsRenderer: React.FC<PropsWithChildren<Props>> = memo(
|
|
|
60
53
|
}
|
|
61
54
|
|
|
62
55
|
return (
|
|
63
|
-
<PluginCellRenderer
|
|
64
|
-
appConfig={appConfig}
|
|
65
|
-
mode={mode}
|
|
66
|
-
plugin={plugin}
|
|
67
|
-
layoutData={layoutData}
|
|
68
|
-
finalLayout={finalLayout}
|
|
69
|
-
/>
|
|
56
|
+
<PluginCellRenderer appConfig={appConfig} mode={mode} plugin={plugin} />
|
|
70
57
|
);
|
|
71
58
|
},
|
|
72
59
|
);
|
|
@@ -77,15 +64,15 @@ interface PluginCellRendererProps extends PropsWithChildren<Props> {
|
|
|
77
64
|
mode: AppMode;
|
|
78
65
|
// oxlint-disable-next-line typescript/no-explicit-any
|
|
79
66
|
plugin: ICellRendererPlugin<any, any>;
|
|
80
|
-
layoutData: Partial<Record<LayoutType, LayoutData>>;
|
|
81
|
-
finalLayout: LayoutType;
|
|
82
67
|
}
|
|
83
68
|
|
|
84
69
|
export const PluginCellRenderer = (props: PluginCellRendererProps) => {
|
|
85
|
-
const { appConfig, mode, plugin
|
|
70
|
+
const { appConfig, mode, plugin } = props;
|
|
71
|
+
const layoutState = useLayoutState();
|
|
86
72
|
const notebook = useNotebook();
|
|
87
73
|
const { setCurrentLayoutData } = useLayoutActions();
|
|
88
74
|
const cells = flattenTopLevelNotebookCells(notebook);
|
|
75
|
+
const layout = resolveLayoutData({ state: layoutState, plugin, cells });
|
|
89
76
|
|
|
90
77
|
const Renderer = plugin.Component;
|
|
91
78
|
const body = (
|
|
@@ -93,7 +80,7 @@ export const PluginCellRenderer = (props: PluginCellRendererProps) => {
|
|
|
93
80
|
appConfig={appConfig}
|
|
94
81
|
mode={mode}
|
|
95
82
|
cells={cells}
|
|
96
|
-
layout={
|
|
83
|
+
layout={layout}
|
|
97
84
|
setLayout={setCurrentLayoutData}
|
|
98
85
|
/>
|
|
99
86
|
);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { MockNotebook } from "@/__mocks__/notebook";
|
|
5
|
+
import { cellId } from "@/__tests__/branded";
|
|
6
|
+
import { deserializeLayout } from "../../plugins";
|
|
7
|
+
|
|
8
|
+
describe("GridLayoutPlugin validation", () => {
|
|
9
|
+
it("preserves valid cell positions while normalizing optional fields", () => {
|
|
10
|
+
const a = cellId("a");
|
|
11
|
+
const b = cellId("b");
|
|
12
|
+
const { cellData } = MockNotebook.notebookState({
|
|
13
|
+
cellData: { [a]: {}, [b]: {} },
|
|
14
|
+
});
|
|
15
|
+
const layout = deserializeLayout({
|
|
16
|
+
type: "grid",
|
|
17
|
+
data: {
|
|
18
|
+
columns: 12,
|
|
19
|
+
rowHeight: 20,
|
|
20
|
+
maxWidth: null,
|
|
21
|
+
cells: [
|
|
22
|
+
{
|
|
23
|
+
position: [0, 0, 6, 4],
|
|
24
|
+
side: "right",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
position: [1, 4, 6, 4],
|
|
28
|
+
side: "future-side",
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
cells: [cellData[a], cellData[b]],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(layout.maxWidth).toBeUndefined();
|
|
36
|
+
expect(layout.cellSide).toEqual(new Map([["a", "right"]]));
|
|
37
|
+
expect(layout.cells).toEqual([
|
|
38
|
+
{ i: "a", x: 0, y: 0, w: 6, h: 4 },
|
|
39
|
+
{ i: "b", x: 1, y: 4, w: 6, h: 4 },
|
|
40
|
+
]);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -24,19 +24,29 @@ export const GridLayoutPlugin: ICellRendererPlugin<
|
|
|
24
24
|
type: "grid",
|
|
25
25
|
name: "Grid",
|
|
26
26
|
|
|
27
|
-
validator: z.
|
|
27
|
+
validator: z.looseObject({
|
|
28
28
|
columns: z.number().min(1),
|
|
29
29
|
rowHeight: z.number().min(1),
|
|
30
|
-
maxWidth: z
|
|
31
|
-
|
|
30
|
+
maxWidth: z
|
|
31
|
+
.number()
|
|
32
|
+
.nullish()
|
|
33
|
+
.transform((value) => value ?? undefined)
|
|
34
|
+
.catch(undefined),
|
|
35
|
+
bordered: z.boolean().optional().catch(undefined),
|
|
32
36
|
cells: z.array(
|
|
33
|
-
z
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
z
|
|
38
|
+
.looseObject({
|
|
39
|
+
position: z
|
|
40
|
+
.tuple([z.number(), z.number(), z.number(), z.number()])
|
|
41
|
+
.nullable()
|
|
42
|
+
.catch(null),
|
|
43
|
+
scrollable: z.boolean().optional().catch(undefined),
|
|
44
|
+
side: z
|
|
45
|
+
.enum(["top", "bottom", "left", "right"])
|
|
46
|
+
.optional()
|
|
47
|
+
.catch(undefined),
|
|
48
|
+
})
|
|
49
|
+
.catch({ position: null }),
|
|
40
50
|
),
|
|
41
51
|
}),
|
|
42
52
|
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
SelectValue,
|
|
18
18
|
} from "@/components/ui/select";
|
|
19
19
|
import { getFeatureFlag } from "@/core/config/feature-flag";
|
|
20
|
-
import { useLayoutActions, useLayoutState } from "@/core/layout/
|
|
20
|
+
import { useLayoutActions, useLayoutState } from "@/core/layout/state";
|
|
21
21
|
import { isWasm } from "@/core/wasm/utils";
|
|
22
22
|
import { logNever } from "@/utils/assertNever";
|
|
23
23
|
import { Strings } from "@/utils/strings";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import type { CellData } from "@/core/cells/types";
|
|
4
|
+
import { Logger } from "@/utils/Logger";
|
|
4
5
|
import { GridLayoutPlugin } from "./grid-layout/plugin";
|
|
5
6
|
import { SlidesLayoutPlugin } from "./slides-layout/plugin";
|
|
6
7
|
import type { ICellRendererPlugin, LayoutType } from "./types";
|
|
@@ -27,5 +28,10 @@ export function deserializeLayout({
|
|
|
27
28
|
if (plugin === undefined) {
|
|
28
29
|
throw new Error(`Unknown layout type: ${type}`);
|
|
29
30
|
}
|
|
30
|
-
|
|
31
|
+
const parsed = plugin.validator.safeParse(data);
|
|
32
|
+
if (!parsed.success) {
|
|
33
|
+
Logger.warn(`Invalid ${type} layout`, parsed.error);
|
|
34
|
+
return plugin.getInitialLayout(cells);
|
|
35
|
+
}
|
|
36
|
+
return plugin.deserializeLayout(parsed.data, cells);
|
|
31
37
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import { describe, it, expect } from "vitest";
|
|
3
|
+
import { describe, it, expect, vi } from "vitest";
|
|
4
4
|
import { SlidesLayoutPlugin } from "../plugin";
|
|
5
5
|
import type { CellData } from "@/core/cells/types";
|
|
6
6
|
import type { CellId } from "@/core/cells/ids";
|
|
7
7
|
import { cellId } from "@/__tests__/branded";
|
|
8
|
+
import { deserializeLayout } from "../../plugins";
|
|
9
|
+
import { Logger } from "@/utils/Logger";
|
|
8
10
|
|
|
9
11
|
function makeCell(id: string, code = "print('hi')"): CellData {
|
|
10
12
|
return {
|
|
@@ -35,12 +37,36 @@ describe("SlidesLayoutPlugin validator", () => {
|
|
|
35
37
|
).toBe(true);
|
|
36
38
|
});
|
|
37
39
|
|
|
38
|
-
it("
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
it("drops unsupported fields while preserving the rest of the layout", () => {
|
|
41
|
+
const layout = deserializeLayout({
|
|
42
|
+
type: "slides",
|
|
43
|
+
data: {
|
|
44
|
+
cells: [
|
|
45
|
+
{
|
|
46
|
+
type: "bogus",
|
|
47
|
+
speakerNotes: "Keep these notes",
|
|
48
|
+
futureCellOption: true,
|
|
49
|
+
},
|
|
50
|
+
{ type: "fragment" },
|
|
51
|
+
],
|
|
52
|
+
deck: {
|
|
53
|
+
transition: "future-transition",
|
|
54
|
+
verticalAlign: "center",
|
|
55
|
+
futureDeckOption: true,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
cells: [makeCell("a"), makeCell("b")],
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(layout.cells.get("a" as CellId)).toEqual({
|
|
62
|
+
speakerNotes: "Keep these notes",
|
|
63
|
+
futureCellOption: true,
|
|
64
|
+
});
|
|
65
|
+
expect(layout.cells.get("b" as CellId)).toEqual({ type: "fragment" });
|
|
66
|
+
expect(layout.deck).toEqual({
|
|
67
|
+
verticalAlign: "center",
|
|
68
|
+
futureDeckOption: true,
|
|
69
|
+
});
|
|
44
70
|
});
|
|
45
71
|
|
|
46
72
|
it("accepts each valid deck vertical alignment", () => {
|
|
@@ -52,12 +78,21 @@ describe("SlidesLayoutPlugin validator", () => {
|
|
|
52
78
|
}
|
|
53
79
|
});
|
|
54
80
|
|
|
55
|
-
it("
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
81
|
+
it("falls back to the initial layout when serialized data is invalid", () => {
|
|
82
|
+
const warning = vi
|
|
83
|
+
.spyOn(Logger, "warn")
|
|
84
|
+
.mockImplementation(() => undefined);
|
|
85
|
+
try {
|
|
86
|
+
expect(
|
|
87
|
+
deserializeLayout({
|
|
88
|
+
type: "slides",
|
|
89
|
+
data: null,
|
|
90
|
+
cells: [makeCell("a")],
|
|
91
|
+
}),
|
|
92
|
+
).toEqual({ cells: new Map(), deck: {} });
|
|
93
|
+
} finally {
|
|
94
|
+
warning.mockRestore();
|
|
95
|
+
}
|
|
61
96
|
});
|
|
62
97
|
});
|
|
63
98
|
|
|
@@ -169,8 +204,7 @@ describe("SlidesLayoutPlugin serializeLayout", () => {
|
|
|
169
204
|
* shape to `serializedSnapshot` so a diff will surface the change in review.
|
|
170
205
|
*
|
|
171
206
|
* Every snapshot is required to:
|
|
172
|
-
* 1. pass the zod validator
|
|
173
|
-
* this guards against regressions once it is),
|
|
207
|
+
* 1. pass the zod validator used during load,
|
|
174
208
|
* 2. deserialize without throwing,
|
|
175
209
|
* 3. survive a deserialize → serialize → deserialize round trip without
|
|
176
210
|
* throwing or losing any user-set field carried in the expectations.
|
|
@@ -8,6 +8,7 @@ import type { ICellRendererProps } from "../types";
|
|
|
8
8
|
import type { SlidesLayout } from "./types";
|
|
9
9
|
import { computeSlideCellsInfo } from "./compute-slide-cells";
|
|
10
10
|
import { SlidesMinimap } from "@/components/slides/minimap";
|
|
11
|
+
import { isSpeakerViewReceiver } from "@/components/slides/speaker-view";
|
|
11
12
|
import useEvent from "react-use-event-hook";
|
|
12
13
|
|
|
13
14
|
type Props = ICellRendererProps<SlidesLayout>;
|
|
@@ -38,6 +39,19 @@ export const SlidesLayoutRenderer: React.FC<Props> = ({
|
|
|
38
39
|
: startCellIndex;
|
|
39
40
|
const resolvedIndex =
|
|
40
41
|
activeSlideIndex === -1 ? startCellIndex : activeSlideIndex;
|
|
42
|
+
const isSpeakerReceiver = isSpeakerViewReceiver(
|
|
43
|
+
kioskMode,
|
|
44
|
+
window.location.search,
|
|
45
|
+
);
|
|
46
|
+
// Let reveal.js resolve an initial URL coordinate before React resumes
|
|
47
|
+
// controlling the deck through the active cell.
|
|
48
|
+
const isInitialDeepLink =
|
|
49
|
+
isReading && activeCellId == null && window.location.hash.startsWith("#/");
|
|
50
|
+
// Speaker-view receivers take their position from reveal.js messages and
|
|
51
|
+
// the URL hash. Controlling their index here can push the presenter back to
|
|
52
|
+
// the first slide while the receiver initializes.
|
|
53
|
+
const controlledIndex =
|
|
54
|
+
isSpeakerReceiver || isInitialDeepLink ? undefined : resolvedIndex;
|
|
41
55
|
|
|
42
56
|
const handleSlideChange = useEvent((index: number) => {
|
|
43
57
|
const cell = slideCells[index];
|
|
@@ -52,8 +66,8 @@ export const SlidesLayoutRenderer: React.FC<Props> = ({
|
|
|
52
66
|
layout={layout}
|
|
53
67
|
setLayout={setLayout}
|
|
54
68
|
noOutputIds={noOutputIds}
|
|
55
|
-
activeIndex={
|
|
56
|
-
onSlideChange={handleSlideChange}
|
|
69
|
+
activeIndex={controlledIndex}
|
|
70
|
+
onSlideChange={isSpeakerReceiver ? undefined : handleSlideChange}
|
|
57
71
|
configWidth={280}
|
|
58
72
|
mode={isReading ? "read" : mode}
|
|
59
73
|
isEditable={!isReading}
|
|
@@ -6,11 +6,13 @@ import type { CellId } from "@/core/cells/ids";
|
|
|
6
6
|
const SlideTypeSchema = z.enum(["slide", "sub-slide", "fragment", "skip"]);
|
|
7
7
|
export type SlideType = z.infer<typeof SlideTypeSchema>;
|
|
8
8
|
|
|
9
|
-
const SlideConfigSchema = z
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
const SlideConfigSchema = z
|
|
10
|
+
.looseObject({
|
|
11
|
+
type: SlideTypeSchema.optional().catch(undefined),
|
|
12
|
+
speakerNotes: z.string().optional().catch(undefined),
|
|
13
|
+
showCode: z.boolean().optional().catch(undefined),
|
|
14
|
+
})
|
|
15
|
+
.catch({});
|
|
14
16
|
export type SlideConfig = z.infer<typeof SlideConfigSchema>;
|
|
15
17
|
|
|
16
18
|
const DeckTransitionSchema = z.enum([
|
|
@@ -26,10 +28,12 @@ export type DeckTransition = z.infer<typeof DeckTransitionSchema>;
|
|
|
26
28
|
const DeckVerticalAlignSchema = z.enum(["top", "center", "bottom"]);
|
|
27
29
|
export type DeckVerticalAlign = z.infer<typeof DeckVerticalAlignSchema>;
|
|
28
30
|
|
|
29
|
-
const DeckConfigSchema = z
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
const DeckConfigSchema = z
|
|
32
|
+
.looseObject({
|
|
33
|
+
transition: DeckTransitionSchema.optional().catch(undefined),
|
|
34
|
+
verticalAlign: DeckVerticalAlignSchema.optional().catch(undefined),
|
|
35
|
+
})
|
|
36
|
+
.catch({});
|
|
33
37
|
export type DeckConfig = z.infer<typeof DeckConfigSchema>;
|
|
34
38
|
|
|
35
39
|
/**
|
|
@@ -41,8 +45,8 @@ export type DeckConfig = z.infer<typeof DeckConfigSchema>;
|
|
|
41
45
|
* keys are preserved (via `looseObject`) for the same reason.
|
|
42
46
|
*/
|
|
43
47
|
export const SlidesLayoutSchema = z.looseObject({
|
|
44
|
-
cells: z.array(SlideConfigSchema).optional(),
|
|
45
|
-
deck: DeckConfigSchema.optional(),
|
|
48
|
+
cells: z.array(SlideConfigSchema).optional().catch(undefined),
|
|
49
|
+
deck: DeckConfigSchema.optional().catch(undefined),
|
|
46
50
|
});
|
|
47
51
|
export type SerializedSlidesLayout = z.infer<typeof SlidesLayoutSchema>;
|
|
48
52
|
|
|
@@ -37,11 +37,11 @@ export interface ICellRendererProps<L> {
|
|
|
37
37
|
setLayout: (layout: L) => void;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
export
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
export const LAYOUT_TYPES = ["vertical", "grid", "slides"] as const;
|
|
41
|
+
export type LayoutType = (typeof LAYOUT_TYPES)[number];
|
|
42
|
+
export function isLayoutType(value: string): value is LayoutType {
|
|
43
|
+
return (LAYOUT_TYPES as readonly string[]).includes(value);
|
|
44
|
+
}
|
|
45
45
|
/**
|
|
46
46
|
* Overridable layout types.
|
|
47
47
|
* These types can override the current layout via a URL parameter.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { useAtomValue } from "jotai/react";
|
|
4
4
|
import { ArrowRightSquareIcon, EyeIcon } from "lucide-react";
|
|
5
5
|
import { KnownQueryParams } from "@/core/constants";
|
|
6
|
-
import { useLayoutState } from "@/core/layout/
|
|
6
|
+
import { useLayoutState } from "@/core/layout/state";
|
|
7
7
|
import { kioskModeAtom, viewStateAtom } from "@/core/mode";
|
|
8
8
|
import { API } from "@/core/network/api";
|
|
9
9
|
import { Banner } from "@/plugins/impl/common/error-banner";
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { Panel, PanelGroup } from "react-resizable-panels";
|
|
4
|
+
import { useAtomValue } from "jotai";
|
|
4
5
|
import { MarimoIcon } from "@/components/icons/marimo-icons";
|
|
5
6
|
import type { AppConfig } from "@/core/config/config-schema";
|
|
6
7
|
import { Constants } from "@/core/constants";
|
|
8
|
+
import { resolveLayoutType, useLayoutState } from "@/core/layout/state";
|
|
7
9
|
import { RunApp } from "@/core/run-app";
|
|
8
|
-
import {
|
|
9
|
-
import { isWasm } from "@/core/wasm/utils";
|
|
10
|
+
import { runtimeAdapterAtom } from "@/core/runtime/adapter";
|
|
10
11
|
import { ContextAwarePanel } from "../editor/chrome/panels/context-aware-panel/context-aware-panel";
|
|
11
12
|
import { PanelsWrapper } from "../editor/chrome/wrapper/panels";
|
|
12
13
|
import { StaticBanner } from "../static-html/static-banner";
|
|
@@ -15,16 +16,24 @@ interface Props {
|
|
|
15
16
|
appConfig: AppConfig;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
const showWatermark = isWasm() || isStaticNotebook();
|
|
19
|
-
|
|
20
19
|
const RunPage = (props: Props) => {
|
|
20
|
+
const runtimeKind = useAtomValue(runtimeAdapterAtom).kind;
|
|
21
|
+
const isExportedNotebook = runtimeKind === "static" || runtimeKind === "wasm";
|
|
22
|
+
const { selectedLayout } = useLayoutState();
|
|
23
|
+
const finalLayout = resolveLayoutType({
|
|
24
|
+
selectedLayout,
|
|
25
|
+
isReading: true,
|
|
26
|
+
searchParams: new URLSearchParams(window.location.search),
|
|
27
|
+
});
|
|
28
|
+
const isExportedSlides = isExportedNotebook && finalLayout === "slides";
|
|
29
|
+
|
|
21
30
|
return (
|
|
22
31
|
<PanelsWrapper>
|
|
23
32
|
<PanelGroup direction="horizontal" autoSaveId="marimo:chrome:v1:run1">
|
|
24
33
|
<Panel>
|
|
25
|
-
<StaticBanner />
|
|
26
|
-
<RunApp appConfig={props.appConfig} />
|
|
27
|
-
{
|
|
34
|
+
{!isExportedSlides && <StaticBanner />}
|
|
35
|
+
<RunApp appConfig={props.appConfig} hideHeader={isExportedSlides} />
|
|
36
|
+
{isExportedNotebook && !isExportedSlides && <Watermark />}
|
|
28
37
|
</Panel>
|
|
29
38
|
<ContextAwarePanel />
|
|
30
39
|
</PanelGroup>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { act, renderHook } from "@testing-library/react";
|
|
4
|
-
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
5
|
import { cellId } from "@/__tests__/branded";
|
|
6
6
|
import type { CellId } from "@/core/cells/ids";
|
|
7
7
|
import type { RuntimeCell } from "@/core/cells/types";
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
deckSlideType,
|
|
11
11
|
parkedRendersSource,
|
|
12
12
|
shouldShowCode,
|
|
13
|
+
syncCodeToggleKeyBinding,
|
|
13
14
|
useParkedPreview,
|
|
14
15
|
} from "../reveal-component";
|
|
15
16
|
|
|
@@ -24,14 +25,38 @@ const cells = (
|
|
|
24
25
|
// to this test helper (see `@/__tests__/branded` for the same rationale).
|
|
25
26
|
const cell = (id: CellId): RuntimeCell => ({ id }) as RuntimeCell;
|
|
26
27
|
|
|
28
|
+
describe("syncCodeToggleKeyBinding", () => {
|
|
29
|
+
it("tracks whether code is available after Reveal is ready", () => {
|
|
30
|
+
const deck = {
|
|
31
|
+
addKeyBinding: vi.fn(),
|
|
32
|
+
removeKeyBinding: vi.fn(),
|
|
33
|
+
};
|
|
34
|
+
const onToggle = vi.fn();
|
|
35
|
+
|
|
36
|
+
syncCodeToggleKeyBinding({ deck, enabled: false, onToggle });
|
|
37
|
+
expect(deck.removeKeyBinding).toHaveBeenLastCalledWith(67);
|
|
38
|
+
expect(deck.addKeyBinding).not.toHaveBeenCalled();
|
|
39
|
+
|
|
40
|
+
syncCodeToggleKeyBinding({ deck, enabled: true, onToggle });
|
|
41
|
+
expect(deck.addKeyBinding).toHaveBeenCalledWith(
|
|
42
|
+
{
|
|
43
|
+
keyCode: 67,
|
|
44
|
+
key: "C",
|
|
45
|
+
description: "Toggle code editor",
|
|
46
|
+
},
|
|
47
|
+
onToggle,
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
27
52
|
describe("shouldShowCode", () => {
|
|
28
|
-
it("is off when
|
|
53
|
+
it("is off when code is unavailable, regardless of config/override", () => {
|
|
29
54
|
expect(
|
|
30
55
|
shouldShowCode({
|
|
31
56
|
cells: cells([A, { showCode: true }]),
|
|
32
57
|
cellId: A,
|
|
33
58
|
showCodeOverrides: new Set([A]),
|
|
34
|
-
|
|
59
|
+
codeAvailable: false,
|
|
35
60
|
}),
|
|
36
61
|
).toBe(false);
|
|
37
62
|
});
|
|
@@ -42,7 +67,7 @@ describe("shouldShowCode", () => {
|
|
|
42
67
|
cells: cells([A, { showCode: true }]),
|
|
43
68
|
cellId: undefined,
|
|
44
69
|
showCodeOverrides: new Set(),
|
|
45
|
-
|
|
70
|
+
codeAvailable: true,
|
|
46
71
|
}),
|
|
47
72
|
).toBe(false);
|
|
48
73
|
});
|
|
@@ -53,7 +78,7 @@ describe("shouldShowCode", () => {
|
|
|
53
78
|
cells: cells([A, { showCode: true }]),
|
|
54
79
|
cellId: A,
|
|
55
80
|
showCodeOverrides: new Set(),
|
|
56
|
-
|
|
81
|
+
codeAvailable: true,
|
|
57
82
|
}),
|
|
58
83
|
).toBe(true);
|
|
59
84
|
// Missing config entry defaults to off.
|
|
@@ -62,7 +87,7 @@ describe("shouldShowCode", () => {
|
|
|
62
87
|
cells: cells(),
|
|
63
88
|
cellId: A,
|
|
64
89
|
showCodeOverrides: new Set(),
|
|
65
|
-
|
|
90
|
+
codeAvailable: true,
|
|
66
91
|
}),
|
|
67
92
|
).toBe(false);
|
|
68
93
|
});
|
|
@@ -74,7 +99,7 @@ describe("shouldShowCode", () => {
|
|
|
74
99
|
cells: cells(),
|
|
75
100
|
cellId: A,
|
|
76
101
|
showCodeOverrides: new Set([A]),
|
|
77
|
-
|
|
102
|
+
codeAvailable: true,
|
|
78
103
|
}),
|
|
79
104
|
).toBe(true);
|
|
80
105
|
// Configured + override present -> still shown; the override never hides.
|
|
@@ -83,7 +108,7 @@ describe("shouldShowCode", () => {
|
|
|
83
108
|
cells: cells([A, { showCode: true }]),
|
|
84
109
|
cellId: A,
|
|
85
110
|
showCodeOverrides: new Set([A]),
|
|
86
|
-
|
|
111
|
+
codeAvailable: true,
|
|
87
112
|
}),
|
|
88
113
|
).toBe(true);
|
|
89
114
|
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import {
|
|
5
|
+
createSpeakerViewUrl,
|
|
6
|
+
isSpeakerViewReceiver,
|
|
7
|
+
supportsSpeakerView,
|
|
8
|
+
} from "../speaker-view";
|
|
9
|
+
|
|
10
|
+
describe("speaker view policy", () => {
|
|
11
|
+
it("builds a receiver base URL independently of the active Reveal hash", () => {
|
|
12
|
+
expect(
|
|
13
|
+
createSpeakerViewUrl("https://example.test/deck?theme=dark#/2/1"),
|
|
14
|
+
).toBe("https://example.test/deck?theme=dark&kiosk=true&show-chrome=false");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("recognizes kiosk receiver frames", () => {
|
|
18
|
+
expect(isSpeakerViewReceiver(true, "?kiosk=true&receiver")).toBe(true);
|
|
19
|
+
expect(isSpeakerViewReceiver(false, "?receiver")).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("keeps speaker view on shared and static runtimes", () => {
|
|
23
|
+
expect(supportsSpeakerView("remote")).toBe(true);
|
|
24
|
+
expect(supportsSpeakerView("static")).toBe(true);
|
|
25
|
+
expect(supportsSpeakerView("wasm")).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
});
|