@marimo-team/islands 0.23.5-dev5 → 0.23.5-dev7

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.
@@ -72,3 +72,11 @@
72
72
  .reveal .mo-slide-content .output {
73
73
  margin: 0;
74
74
  }
75
+
76
+ .reveal .marimo-cell .cm-editor {
77
+ border-radius: 8px;
78
+ padding-right: 10px;
79
+ }
80
+ .reveal .marimo-cell .cm-panels {
81
+ margin-right: 0;
82
+ }
@@ -0,0 +1,182 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { useMemo, useRef, useState } from "react";
4
+ import type { EditorView } from "@codemirror/view";
5
+ import { useAtomValue } from "jotai";
6
+ import { CellEditor } from "@/components/editor/cell/code/cell-editor";
7
+ import { CellStatusComponent } from "@/components/editor/cell/CellStatus";
8
+ import { RunButton } from "@/components/editor/cell/RunButton";
9
+ import { StopButton } from "@/components/editor/cell/StopButton";
10
+ import { useRunCell } from "@/components/editor/cell/useRunCells";
11
+ import { Slide as CellOutputSlide } from "@/components/slides/slide";
12
+ import { useUserConfig } from "@/core/config/config";
13
+ import {
14
+ cellNeedsRun,
15
+ cellStatusClasses,
16
+ isUninstantiated,
17
+ } from "@/core/cells/utils";
18
+ import type { CellData, CellRuntimeState } from "@/core/cells/types";
19
+ import type { LanguageAdapterType } from "@/core/codemirror/language/types";
20
+ import { connectionAtom } from "@/core/network/connection";
21
+ import { useTheme } from "@/theme/useTheme";
22
+ import { cn } from "@/utils/cn";
23
+ import { ReadonlyCode } from "../editor/code/readonly-python-code";
24
+ import { languageAdapterFromCode } from "@/core/codemirror/language/extension";
25
+
26
+ type RuntimeCell = CellRuntimeState & CellData;
27
+
28
+ /**
29
+ * Renders a single cell in the slides view as an editable CodeMirror editor
30
+ * stacked with its output, mirroring the notebook layout. Editing and
31
+ * Ctrl/Cmd+Enter run the cell against the live kernel so presenters can iterate
32
+ * without leaving the slide deck.
33
+ */
34
+ export const SlideCellView = ({ cell }: { cell: RuntimeCell }) => {
35
+ const [userConfig] = useUserConfig();
36
+ const { theme } = useTheme();
37
+ const runCell = useRunCell(cell.id);
38
+ const connection = useAtomValue(connectionAtom);
39
+ const editorViewRef = useRef<EditorView | null>(null);
40
+ const editorViewParentRef = useRef<HTMLDivElement | null>(null);
41
+ const [languageAdapter, setLanguageAdapter] = useState<
42
+ LanguageAdapterType | undefined
43
+ >();
44
+
45
+ const cellOutputPosition = userConfig.display.cell_output;
46
+ const hasOutput = cell.output != null;
47
+
48
+ const uninstantiated = isUninstantiated({
49
+ executionTime: cell.runElapsedTimeMs ?? cell.lastExecutionTime,
50
+ status: cell.status,
51
+ errored: cell.errored,
52
+ interrupted: cell.interrupted,
53
+ stopped: cell.stopped,
54
+ });
55
+
56
+ const needsRun = cellNeedsRun({
57
+ edited: cell.edited,
58
+ interrupted: cell.interrupted,
59
+ staleInputs: cell.staleInputs,
60
+ disabled: cell.config.disabled,
61
+ status: cell.status,
62
+ });
63
+
64
+ const editorWrapperClassName = cn(
65
+ "marimo-cell",
66
+ "hover-actions-parent",
67
+ "interactive",
68
+ cellStatusClasses({
69
+ needsRun,
70
+ errored: cell.errored,
71
+ stopped: cell.stopped,
72
+ disabled: cell.config.disabled,
73
+ status: cell.status,
74
+ }),
75
+ );
76
+
77
+ const output = (
78
+ <CellOutputSlide
79
+ cellId={cell.id}
80
+ status={cell.status}
81
+ output={cell.output}
82
+ />
83
+ );
84
+
85
+ const toolbar = (
86
+ <div className="absolute top-1 right-2 z-10 flex items-center gap-1.5">
87
+ <CellStatusComponent
88
+ editing={true}
89
+ status={cell.status}
90
+ disabled={cell.config.disabled ?? false}
91
+ staleInputs={cell.staleInputs}
92
+ edited={cell.edited}
93
+ interrupted={cell.interrupted}
94
+ elapsedTime={cell.runElapsedTimeMs ?? cell.lastExecutionTime}
95
+ runStartTimestamp={cell.runStartTimestamp}
96
+ lastRunStartTimestamp={cell.lastRunStartTimestamp}
97
+ uninstantiated={uninstantiated}
98
+ />
99
+ <div className="flex items-center shadow-none gap-1">
100
+ <RunButton
101
+ edited={cell.edited}
102
+ status={cell.status}
103
+ needsRun={needsRun}
104
+ connectionState={connection.state}
105
+ config={cell.config}
106
+ onClick={runCell}
107
+ />
108
+ <StopButton status={cell.status} connectionState={connection.state} />
109
+ </div>
110
+ </div>
111
+ );
112
+
113
+ const editor = (
114
+ <div className={editorWrapperClassName}>
115
+ <CellEditor
116
+ theme={theme}
117
+ showPlaceholder={false}
118
+ id={cell.id}
119
+ code={cell.code}
120
+ config={cell.config}
121
+ status={cell.status}
122
+ serializedEditorState={cell.serializedEditorState}
123
+ runCell={runCell}
124
+ setEditorView={(ev) => {
125
+ editorViewRef.current = ev;
126
+ }}
127
+ userConfig={userConfig}
128
+ editorViewRef={editorViewRef}
129
+ editorViewParentRef={editorViewParentRef}
130
+ hasOutput={hasOutput}
131
+ // hide_code is intentionally overridden in the slide view; the editor
132
+ // is unmounted entirely when the user toggles code off.
133
+ showHiddenCode={() => undefined}
134
+ languageAdapter={languageAdapter}
135
+ setLanguageAdapter={setLanguageAdapter}
136
+ showLanguageToggles={false}
137
+ outputArea={cellOutputPosition}
138
+ />
139
+ {toolbar}
140
+ </div>
141
+ );
142
+
143
+ return (
144
+ <>
145
+ {cellOutputPosition === "above" && output}
146
+ {editor}
147
+ {cellOutputPosition === "below" && output}
148
+ </>
149
+ );
150
+ };
151
+
152
+ export const SlideCellReadOnlyView = ({ cell }: { cell: RuntimeCell }) => {
153
+ const [userConfig] = useUserConfig();
154
+ const cellOutputPosition = userConfig.display.cell_output;
155
+
156
+ const language = useMemo(() => {
157
+ const adapter = languageAdapterFromCode(cell.code.trim());
158
+ return adapter.type === "sql" ? "sql" : "python";
159
+ }, [cell.code]);
160
+
161
+ const output = (
162
+ <CellOutputSlide
163
+ cellId={cell.id}
164
+ status={cell.status}
165
+ output={cell.output}
166
+ />
167
+ );
168
+
169
+ const editor = (
170
+ <div className="marimo-cell">
171
+ <ReadonlyCode code={cell.code} language={language} showHideCode={false} />
172
+ </div>
173
+ );
174
+
175
+ return (
176
+ <>
177
+ {cellOutputPosition === "above" && output}
178
+ {editor}
179
+ {cellOutputPosition === "below" && output}
180
+ </>
181
+ );
182
+ };
@@ -288,6 +288,8 @@ export const SlideSidebar = ({
288
288
  width: isConfigOpen ? configWidth : COLLAPSED_CONFIG_WIDTH,
289
289
  }}
290
290
  aria-label="Slide configuration"
291
+ // Prevent keys from bubbling up to reveal.js's document-level keydown listener and moving the deck.
292
+ onKeyDown={(e) => e.stopPropagation()}
291
293
  >
292
294
  <header
293
295
  className={cn(
@@ -140,3 +140,48 @@ export function isUninstantiated({
140
140
  !(errored || interrupted || stopped)
141
141
  );
142
142
  }
143
+
144
+ /**
145
+ * Whether a cell needs to be run given its edited / interrupted / stale
146
+ * inputs, while accounting for ancestor-disabled cells (which should not be
147
+ * flagged as needing a run until re-enabled).
148
+ */
149
+ export function cellNeedsRun({
150
+ edited,
151
+ interrupted,
152
+ staleInputs,
153
+ disabled,
154
+ status,
155
+ }: {
156
+ edited: boolean;
157
+ interrupted: boolean;
158
+ staleInputs: boolean;
159
+ disabled: boolean | undefined;
160
+ status: RuntimeState;
161
+ }): boolean {
162
+ const disabledOrAncestorDisabled =
163
+ disabled || status === "disabled-transitively";
164
+ return edited || interrupted || (staleInputs && !disabledOrAncestorDisabled);
165
+ }
166
+
167
+ export function cellStatusClasses({
168
+ needsRun,
169
+ errored,
170
+ stopped,
171
+ disabled,
172
+ status,
173
+ }: {
174
+ needsRun: boolean;
175
+ errored: boolean;
176
+ stopped: boolean;
177
+ disabled: boolean | undefined;
178
+ status: RuntimeState;
179
+ }) {
180
+ return {
181
+ "needs-run": needsRun,
182
+ "has-error": errored,
183
+ stopped,
184
+ disabled: disabled ?? false,
185
+ stale: status === "disabled-transitively",
186
+ };
187
+ }
@@ -0,0 +1,30 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import type { RuntimeCell } from "@/core/cells/types";
4
+ import { Logger } from "@/utils/Logger";
5
+
6
+ /**
7
+ * Build-time stub for `@/components/slides/slide-cell-view`, wired up via
8
+ * `resolve.alias` in `frontend/islands/vite.config.mts`. Islands embeds gate
9
+ * off the slides "show code" toggle entirely (see `useNotebookCodeAvailable`
10
+ * + `isIslands()`), so neither view is reachable at runtime there.
11
+ *
12
+ * Replacing the module at build time keeps the entire CodeMirror /
13
+ * Codeium / `@bufbuild/protobuf` import subtree out of the islands bundle,
14
+ * which both shrinks the bundle and lets `islands/validate.sh` pass — the
15
+ * upstream protobuf code contains a `process.env.BUF_BIGINT_DISABLE`
16
+ * runtime check that the validator otherwise flags.
17
+ */
18
+ export const SlideCellView = (_props: { cell: RuntimeCell }) => {
19
+ Logger.warn(
20
+ "SlideCellView islands stub rendered; this should never happen in a read-only embed.",
21
+ );
22
+ return null;
23
+ };
24
+
25
+ export const SlideCellReadOnlyView = (_props: { cell: RuntimeCell }) => {
26
+ Logger.warn(
27
+ "SlideCellReadOnlyView islands stub rendered; this should never happen in a read-only embed.",
28
+ );
29
+ return null;
30
+ };
@@ -0,0 +1,141 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { renderHook } from "@testing-library/react";
4
+ import { createStore, Provider } from "jotai";
5
+ import type { ReactNode } from "react";
6
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
7
+ import { showCodeInRunModeAtom } from "@/core/meta/state";
8
+ import { type AppMode, kioskModeAtom, viewStateAtom } from "@/core/mode";
9
+ import { useNotebookCodeAvailable } from "../code-visibility";
10
+
11
+ interface StoreOpts {
12
+ mode?: AppMode;
13
+ kiosk?: boolean;
14
+ showInRunMode?: boolean;
15
+ }
16
+
17
+ function makeStore({
18
+ mode = "read",
19
+ kiosk = false,
20
+ showInRunMode = true,
21
+ }: StoreOpts = {}) {
22
+ const store = createStore();
23
+ store.set(viewStateAtom, { mode, cellAnchor: null });
24
+ store.set(kioskModeAtom, kiosk);
25
+ store.set(showCodeInRunModeAtom, showInRunMode);
26
+ return store;
27
+ }
28
+
29
+ function wrap(store: ReturnType<typeof createStore>) {
30
+ return ({ children }: { children: ReactNode }) => (
31
+ <Provider store={store}>{children}</Provider>
32
+ );
33
+ }
34
+
35
+ const cellsWithCode = [{ code: "x = 1" }, { code: "" }];
36
+ const cellsWithoutCode = [{ code: "" }, { code: "" }];
37
+
38
+ const originalHref = window.location.href;
39
+
40
+ function setSearch(search: string) {
41
+ window.history.replaceState(null, "", `/${search}`);
42
+ }
43
+
44
+ describe("useNotebookCodeAvailable", () => {
45
+ beforeEach(() => {
46
+ setSearch("");
47
+ });
48
+
49
+ afterEach(() => {
50
+ window.history.replaceState(null, "", originalHref);
51
+ });
52
+
53
+ it("returns true in edit mode regardless of cells", () => {
54
+ const store = makeStore({ mode: "edit" });
55
+ const { result } = renderHook(
56
+ () => useNotebookCodeAvailable(cellsWithoutCode),
57
+ { wrapper: wrap(store) },
58
+ );
59
+ expect(result.current).toBe(true);
60
+ });
61
+
62
+ it("returns true in present mode (only reachable from edit)", () => {
63
+ const store = makeStore({ mode: "present" });
64
+ const { result } = renderHook(
65
+ () => useNotebookCodeAvailable(cellsWithoutCode),
66
+ { wrapper: wrap(store) },
67
+ );
68
+ expect(result.current).toBe(true);
69
+ });
70
+
71
+ it("returns true in kiosk mode even when read mode and no code", () => {
72
+ const store = makeStore({
73
+ mode: "read",
74
+ kiosk: true,
75
+ showInRunMode: false,
76
+ });
77
+ const { result } = renderHook(
78
+ () => useNotebookCodeAvailable(cellsWithoutCode),
79
+ { wrapper: wrap(store) },
80
+ );
81
+ expect(result.current).toBe(true);
82
+ });
83
+
84
+ it("returns true in read mode when at least one cell has code", () => {
85
+ const store = makeStore({ mode: "read" });
86
+ const { result } = renderHook(
87
+ () => useNotebookCodeAvailable(cellsWithCode),
88
+ { wrapper: wrap(store) },
89
+ );
90
+ expect(result.current).toBe(true);
91
+ });
92
+
93
+ it("returns false in read mode when every cell.code is empty (server stripped)", () => {
94
+ const store = makeStore({ mode: "read" });
95
+ const { result } = renderHook(
96
+ () => useNotebookCodeAvailable(cellsWithoutCode),
97
+ { wrapper: wrap(store) },
98
+ );
99
+ expect(result.current).toBe(false);
100
+ });
101
+
102
+ it("returns false in read mode when host opts out via showAppCode", () => {
103
+ const store = makeStore({ mode: "read", showInRunMode: false });
104
+ const { result } = renderHook(
105
+ () => useNotebookCodeAvailable(cellsWithCode),
106
+ { wrapper: wrap(store) },
107
+ );
108
+ expect(result.current).toBe(false);
109
+ });
110
+
111
+ it("returns false in read mode when ?include-code=false", () => {
112
+ setSearch("?include-code=false");
113
+ const store = makeStore({ mode: "read" });
114
+ const { result } = renderHook(
115
+ () => useNotebookCodeAvailable(cellsWithCode),
116
+ { wrapper: wrap(store) },
117
+ );
118
+ expect(result.current).toBe(false);
119
+ });
120
+
121
+ it("ignores ?include-code=false in kiosk mode", () => {
122
+ setSearch("?include-code=false");
123
+ const store = makeStore({ mode: "read", kiosk: true });
124
+ const { result } = renderHook(
125
+ () => useNotebookCodeAvailable(cellsWithoutCode),
126
+ { wrapper: wrap(store) },
127
+ );
128
+ expect(result.current).toBe(true);
129
+ });
130
+
131
+ it("returns false for non-edit/non-read modes (home, gallery)", () => {
132
+ for (const mode of ["home", "gallery"] as const) {
133
+ const store = makeStore({ mode });
134
+ const { result } = renderHook(
135
+ () => useNotebookCodeAvailable(cellsWithCode),
136
+ { wrapper: wrap(store) },
137
+ );
138
+ expect(result.current).toBe(false);
139
+ }
140
+ });
141
+ });
@@ -0,0 +1,48 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { useAtomValue } from "jotai";
4
+ import { KnownQueryParams } from "@/core/constants";
5
+ import { showCodeInRunModeAtom } from "@/core/meta/state";
6
+ import { kioskModeAtom, viewStateAtom } from "@/core/mode";
7
+ import { logNever } from "@/utils/assertNever";
8
+
9
+ /**
10
+ * Whether the notebook source code reached the frontend and can be rendered.
11
+ *
12
+ * In `marimo run` the server omits cell sources unless `--include-code` is
13
+ * set (every `cell.code` arrives as `""`). Use this to gate any "show code"
14
+ * / "copy code" / "download .py" affordance.
15
+ */
16
+ export function useNotebookCodeAvailable(
17
+ cells: ReadonlyArray<{ code: string }>,
18
+ ): boolean {
19
+ const kioskMode = useAtomValue(kioskModeAtom);
20
+ const { mode } = useAtomValue(viewStateAtom);
21
+ const showInRunMode = useAtomValue(showCodeInRunModeAtom);
22
+
23
+ if (kioskMode) {
24
+ return true;
25
+ }
26
+
27
+ switch (mode) {
28
+ case "edit":
29
+ case "present":
30
+ return true;
31
+ case "home":
32
+ case "gallery":
33
+ return false;
34
+ case "read": {
35
+ if (!showInRunMode) {
36
+ return false;
37
+ }
38
+ const params = new URLSearchParams(window.location.search);
39
+ if (params.get(KnownQueryParams.includeCode) === "false") {
40
+ return false;
41
+ }
42
+ return cells.some((cell) => Boolean(cell.code));
43
+ }
44
+ default:
45
+ logNever(mode);
46
+ return false;
47
+ }
48
+ }