@marimo-team/frontend 0.23.1-dev9 → 0.23.1
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/{JsonOutput-BY31ccA7.js → JsonOutput-CavtrueA.js} +1 -1
- package/dist/assets/{MarimoErrorOutput--Yd2Aw0J.js → MarimoErrorOutput-Bmp8DLLo.js} +1 -1
- package/dist/assets/RenderHTML-CM3WMmA8.js +1 -0
- package/dist/assets/{add-connection-dialog-CjvNOKgb.js → add-connection-dialog-BGZvJkor.js} +1 -1
- package/dist/assets/{agent-panel-C24uwabG.js → agent-panel-BvL9Lu9c.js} +1 -1
- package/dist/assets/{cell-editor-zW0u82sK.js → cell-editor-B40o_zx_.js} +1 -1
- package/dist/assets/{chat-display-DsHMZa9F.js → chat-display-M_nvYuHH.js} +1 -1
- package/dist/assets/{chat-panel-o9D3upnX.js → chat-panel-BMOW93uQ.js} +1 -1
- package/dist/assets/{chat-ui-BYS03y86.js → chat-ui-DyeimpVh.js} +1 -1
- package/dist/assets/{column-preview-Dwv5a_zE.js → column-preview-AfcgbFG_.js} +1 -1
- package/dist/assets/{command-palette-BYbKGSF3.js → command-palette-BgvdyU3B.js} +1 -1
- package/dist/assets/{documentation-panel-CA2pWMgB.js → documentation-panel-DUPcsi8P.js} +1 -1
- package/dist/assets/{edit-page-CMUN3ESy.js → edit-page-DD4uEDmX.js} +4 -4
- package/dist/assets/{error-panel-CbqfK1HJ.js → error-panel-DQOeSv5-.js} +1 -1
- package/dist/assets/{file-explorer-panel-CbS8z-JR.js → file-explorer-panel-B67zjs2X.js} +1 -1
- package/dist/assets/{form-DLyXacSF.js → form-BJ6VFU8l.js} +1 -1
- package/dist/assets/{hooks-kZJc1iBf.js → hooks-DvwShzDb.js} +1 -1
- package/dist/assets/index-y6osgSWB.js +42 -0
- package/dist/assets/{layout-tmN-U1zs.js → layout-erv8pLIP.js} +1 -1
- package/dist/assets/{panels-CLfdzLPR.js → panels-1u-RE72f.js} +1 -1
- package/dist/assets/{run-page-DPuH6QY4.js → run-page-DfWH_1mz.js} +1 -1
- package/dist/assets/{scratchpad-panel-BsMm0GQP.js → scratchpad-panel-CnaiXtoJ.js} +1 -1
- package/dist/assets/{session-panel-CTDzGShO.js → session-panel-C68GBFwH.js} +1 -1
- package/dist/assets/{snippets-panel-CWof0wHk.js → snippets-panel-BmIdR0lc.js} +1 -1
- package/dist/assets/state-D1n-olwf.js +3 -0
- package/dist/assets/{useNotebookActions-DHBEqrc_.js → useNotebookActions-Ch1o32Jw.js} +1 -1
- package/dist/index.html +7 -7
- package/package.json +4 -4
- package/src/core/islands/__tests__/bridge.test.ts +2 -12
- package/src/core/islands/__tests__/islands-harness.test.ts +348 -0
- package/src/core/islands/__tests__/parse.test.ts +466 -24
- package/src/core/islands/__tests__/test-utils.tsx +263 -0
- package/src/core/islands/bootstrap.ts +265 -0
- package/src/core/islands/bridge.ts +154 -75
- package/src/core/islands/components/IslandControls.tsx +103 -0
- package/src/core/islands/components/__tests__/IslandControls.test.tsx +185 -0
- package/src/core/islands/components/__tests__/useIslandControls.test.ts +208 -0
- package/src/core/islands/components/output-wrapper.tsx +76 -93
- package/src/core/islands/components/useIslandControls.ts +60 -0
- package/src/core/islands/components/web-components.tsx +168 -40
- package/src/core/islands/constants.ts +28 -0
- package/src/core/islands/main.ts +7 -205
- package/src/core/islands/parse.ts +73 -26
- package/src/core/islands/worker-factory.ts +86 -0
- package/src/plugins/core/RenderHTML.tsx +9 -0
- package/src/plugins/core/__test__/RenderHTML.test.ts +27 -0
- package/src/plugins/core/__test__/trusted-url.test.ts +48 -0
- package/src/plugins/core/registerReactComponent.tsx +11 -8
- package/src/plugins/core/trusted-url.ts +20 -0
- package/src/plugins/impl/ButtonPlugin.tsx +4 -6
- package/src/plugins/impl/CodeEditorPlugin.tsx +15 -18
- package/src/plugins/impl/DataEditorPlugin.tsx +8 -14
- package/src/plugins/impl/DataTablePlugin.tsx +8 -9
- package/src/plugins/impl/FileUploadPlugin.tsx +39 -43
- package/src/plugins/impl/FormPlugin.tsx +2 -6
- package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +27 -1
- package/src/plugins/impl/anywidget/widget-binding.ts +13 -0
- package/src/plugins/impl/chat/ChatPlugin.tsx +17 -20
- package/src/plugins/impl/data-explorer/DataExplorerPlugin.tsx +5 -8
- package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +21 -0
- package/src/plugins/impl/mpl-interactive/__tests__/MplInteractivePlugin.test.tsx +119 -0
- package/src/plugins/impl/panel/PanelPlugin.tsx +31 -10
- package/src/plugins/impl/panel/__tests__/PanelPlugin.test.ts +60 -0
- package/src/plugins/impl/vega/VegaPlugin.tsx +5 -8
- package/src/plugins/layout/NavigationMenuPlugin.tsx +2 -6
- package/dist/assets/RenderHTML-CbuarQqA.js +0 -1
- package/dist/assets/index-Bm25ctN7.js +0 -42
- package/dist/assets/state-BvnlMKdT.js +0 -3
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/* Copyright 2024 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { act, renderHook } from "@testing-library/react";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { useIslandControls } from "../useIslandControls";
|
|
6
|
+
|
|
7
|
+
describe("useIslandControls", () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
// Clean up any event listeners
|
|
10
|
+
document.body.innerHTML = "";
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
// Clean up
|
|
15
|
+
document.body.innerHTML = "";
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should return false initially when alwaysShowRun is false", () => {
|
|
19
|
+
const { result } = renderHook(() => useIslandControls(false));
|
|
20
|
+
expect(result.current).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("should return true initially when alwaysShowRun is true", () => {
|
|
24
|
+
const { result } = renderHook(() => useIslandControls(true));
|
|
25
|
+
expect(result.current).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should return true when Cmd key is pressed (macOS)", () => {
|
|
29
|
+
const { result } = renderHook(() => useIslandControls(false));
|
|
30
|
+
|
|
31
|
+
expect(result.current).toBe(false);
|
|
32
|
+
|
|
33
|
+
act(() => {
|
|
34
|
+
const event = new KeyboardEvent("keydown", { metaKey: true });
|
|
35
|
+
document.dispatchEvent(event);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
expect(result.current).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should return true when Ctrl key is pressed (Windows/Linux)", () => {
|
|
42
|
+
const { result } = renderHook(() => useIslandControls(false));
|
|
43
|
+
|
|
44
|
+
expect(result.current).toBe(false);
|
|
45
|
+
|
|
46
|
+
act(() => {
|
|
47
|
+
const event = new KeyboardEvent("keydown", { ctrlKey: true });
|
|
48
|
+
document.dispatchEvent(event);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(result.current).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should return false when Cmd key is released", () => {
|
|
55
|
+
const { result } = renderHook(() => useIslandControls(false));
|
|
56
|
+
|
|
57
|
+
// Press Cmd
|
|
58
|
+
act(() => {
|
|
59
|
+
const event = new KeyboardEvent("keydown", { metaKey: true });
|
|
60
|
+
document.dispatchEvent(event);
|
|
61
|
+
});
|
|
62
|
+
expect(result.current).toBe(true);
|
|
63
|
+
|
|
64
|
+
// Release Cmd
|
|
65
|
+
act(() => {
|
|
66
|
+
const event = new KeyboardEvent("keyup", { metaKey: true });
|
|
67
|
+
document.dispatchEvent(event);
|
|
68
|
+
});
|
|
69
|
+
expect(result.current).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should return false when Ctrl key is released", () => {
|
|
73
|
+
const { result } = renderHook(() => useIslandControls(false));
|
|
74
|
+
|
|
75
|
+
// Press Ctrl
|
|
76
|
+
act(() => {
|
|
77
|
+
const event = new KeyboardEvent("keydown", { ctrlKey: true });
|
|
78
|
+
document.dispatchEvent(event);
|
|
79
|
+
});
|
|
80
|
+
expect(result.current).toBe(true);
|
|
81
|
+
|
|
82
|
+
// Release Ctrl
|
|
83
|
+
act(() => {
|
|
84
|
+
const event = new KeyboardEvent("keyup", { ctrlKey: true });
|
|
85
|
+
document.dispatchEvent(event);
|
|
86
|
+
});
|
|
87
|
+
expect(result.current).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("should return false when Meta key name is released", () => {
|
|
91
|
+
const { result } = renderHook(() => useIslandControls(false));
|
|
92
|
+
|
|
93
|
+
// Press Cmd
|
|
94
|
+
act(() => {
|
|
95
|
+
const event = new KeyboardEvent("keydown", { metaKey: true });
|
|
96
|
+
document.dispatchEvent(event);
|
|
97
|
+
});
|
|
98
|
+
expect(result.current).toBe(true);
|
|
99
|
+
|
|
100
|
+
// Release by key name
|
|
101
|
+
act(() => {
|
|
102
|
+
const event = new KeyboardEvent("keyup", { key: "Meta" });
|
|
103
|
+
document.dispatchEvent(event);
|
|
104
|
+
});
|
|
105
|
+
expect(result.current).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should return false when Control key name is released", () => {
|
|
109
|
+
const { result } = renderHook(() => useIslandControls(false));
|
|
110
|
+
|
|
111
|
+
// Press Ctrl
|
|
112
|
+
act(() => {
|
|
113
|
+
const event = new KeyboardEvent("keydown", { ctrlKey: true });
|
|
114
|
+
document.dispatchEvent(event);
|
|
115
|
+
});
|
|
116
|
+
expect(result.current).toBe(true);
|
|
117
|
+
|
|
118
|
+
// Release by key name
|
|
119
|
+
act(() => {
|
|
120
|
+
const event = new KeyboardEvent("keyup", { key: "Control" });
|
|
121
|
+
document.dispatchEvent(event);
|
|
122
|
+
});
|
|
123
|
+
expect(result.current).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("should return false when window loses focus", () => {
|
|
127
|
+
const { result } = renderHook(() => useIslandControls(false));
|
|
128
|
+
|
|
129
|
+
// Press Cmd
|
|
130
|
+
act(() => {
|
|
131
|
+
const event = new KeyboardEvent("keydown", { metaKey: true });
|
|
132
|
+
document.dispatchEvent(event);
|
|
133
|
+
});
|
|
134
|
+
expect(result.current).toBe(true);
|
|
135
|
+
|
|
136
|
+
// Blur window
|
|
137
|
+
act(() => {
|
|
138
|
+
const event = new Event("blur");
|
|
139
|
+
window.dispatchEvent(event);
|
|
140
|
+
});
|
|
141
|
+
expect(result.current).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("should return false when mouse leaves window", () => {
|
|
145
|
+
const { result } = renderHook(() => useIslandControls(false));
|
|
146
|
+
|
|
147
|
+
// Press Cmd
|
|
148
|
+
act(() => {
|
|
149
|
+
const event = new KeyboardEvent("keydown", { metaKey: true });
|
|
150
|
+
document.dispatchEvent(event);
|
|
151
|
+
});
|
|
152
|
+
expect(result.current).toBe(true);
|
|
153
|
+
|
|
154
|
+
// Mouse leave
|
|
155
|
+
act(() => {
|
|
156
|
+
const event = new Event("mouseleave");
|
|
157
|
+
window.dispatchEvent(event);
|
|
158
|
+
});
|
|
159
|
+
expect(result.current).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("should not register keyboard event listeners when alwaysShowRun is true", () => {
|
|
163
|
+
const { result } = renderHook(() => useIslandControls(true));
|
|
164
|
+
|
|
165
|
+
expect(result.current).toBe(true);
|
|
166
|
+
|
|
167
|
+
// Try to change state with keyboard events - should have no effect
|
|
168
|
+
act(() => {
|
|
169
|
+
const event = new KeyboardEvent("keydown", { metaKey: true });
|
|
170
|
+
document.dispatchEvent(event);
|
|
171
|
+
});
|
|
172
|
+
expect(result.current).toBe(true);
|
|
173
|
+
|
|
174
|
+
act(() => {
|
|
175
|
+
const event = new KeyboardEvent("keyup", { metaKey: true });
|
|
176
|
+
document.dispatchEvent(event);
|
|
177
|
+
});
|
|
178
|
+
expect(result.current).toBe(true);
|
|
179
|
+
|
|
180
|
+
// Blur and mouseleave are also no-ops when alwaysShowRun is true
|
|
181
|
+
act(() => {
|
|
182
|
+
window.dispatchEvent(new Event("blur"));
|
|
183
|
+
});
|
|
184
|
+
expect(result.current).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("should handle rapid key presses correctly", () => {
|
|
188
|
+
const { result } = renderHook(() => useIslandControls(false));
|
|
189
|
+
|
|
190
|
+
expect(result.current).toBe(false);
|
|
191
|
+
|
|
192
|
+
// Multiple rapid presses
|
|
193
|
+
act(() => {
|
|
194
|
+
document.dispatchEvent(new KeyboardEvent("keydown", { metaKey: true }));
|
|
195
|
+
document.dispatchEvent(new KeyboardEvent("keydown", { metaKey: true }));
|
|
196
|
+
document.dispatchEvent(new KeyboardEvent("keydown", { metaKey: true }));
|
|
197
|
+
});
|
|
198
|
+
expect(result.current).toBe(true);
|
|
199
|
+
|
|
200
|
+
// Multiple rapid releases
|
|
201
|
+
act(() => {
|
|
202
|
+
document.dispatchEvent(new KeyboardEvent("keyup", { metaKey: true }));
|
|
203
|
+
document.dispatchEvent(new KeyboardEvent("keyup", { metaKey: true }));
|
|
204
|
+
document.dispatchEvent(new KeyboardEvent("keyup", { metaKey: true }));
|
|
205
|
+
});
|
|
206
|
+
expect(result.current).toBe(false);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -2,139 +2,120 @@
|
|
|
2
2
|
|
|
3
3
|
import { useAtomValue } from "jotai";
|
|
4
4
|
import { selectAtom } from "jotai/utils";
|
|
5
|
-
import {
|
|
5
|
+
import { Loader2Icon } from "lucide-react";
|
|
6
6
|
import React, {
|
|
7
|
-
type JSX,
|
|
8
7
|
type PropsWithChildren,
|
|
9
8
|
useCallback,
|
|
10
|
-
|
|
9
|
+
useEffect,
|
|
10
|
+
useRef,
|
|
11
11
|
} from "react";
|
|
12
12
|
import { OutputRenderer } from "@/components/editor/Output";
|
|
13
|
-
import { Button } from "@/components/ui/button";
|
|
14
|
-
import { Tooltip } from "@/components/ui/tooltip";
|
|
15
13
|
import { type NotebookState, notebookAtom } from "@/core/cells/cells";
|
|
16
14
|
import type { CellId } from "@/core/cells/ids";
|
|
17
15
|
import { isOutputEmpty } from "@/core/cells/outputs";
|
|
18
16
|
import type { CellRuntimeState } from "@/core/cells/types";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import { Logger } from "@/utils/Logger";
|
|
17
|
+
import { ISLAND_TAG_NAMES } from "../constants";
|
|
18
|
+
import { IslandControls } from "./IslandControls";
|
|
19
|
+
import { useIslandControls } from "./useIslandControls";
|
|
23
20
|
|
|
24
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Props for MarimoOutputWrapper component
|
|
23
|
+
*/
|
|
24
|
+
export interface MarimoOutputWrapperProps {
|
|
25
|
+
/**
|
|
26
|
+
* ID of the cell being rendered
|
|
27
|
+
*/
|
|
25
28
|
cellId: CellId;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Callback to get the current code for the cell
|
|
32
|
+
*/
|
|
26
33
|
codeCallback: () => string;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Whether to always show the run button (e.g., when editor is present)
|
|
37
|
+
*/
|
|
27
38
|
alwaysShowRun: boolean;
|
|
28
|
-
children: React.ReactNode;
|
|
29
|
-
}
|
|
30
39
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Initial/static HTML content to display
|
|
42
|
+
*/
|
|
43
|
+
children: React.ReactNode;
|
|
35
44
|
}
|
|
36
45
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
</Button>
|
|
47
|
-
</Tooltip>
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
export const MarimoOutputWrapper: React.FC<Props> = ({
|
|
46
|
+
/**
|
|
47
|
+
* Wraps marimo cell output with interactive controls and status indicators.
|
|
48
|
+
*
|
|
49
|
+
* This component:
|
|
50
|
+
* - Renders cell output from the runtime state
|
|
51
|
+
* - Shows a loading spinner when the cell is running
|
|
52
|
+
* - Provides controls to copy code and re-run the cell
|
|
53
|
+
*/
|
|
54
|
+
export const MarimoOutputWrapper: React.FC<MarimoOutputWrapperProps> = ({
|
|
51
55
|
cellId,
|
|
52
56
|
codeCallback,
|
|
53
57
|
alwaysShowRun,
|
|
54
58
|
children,
|
|
55
59
|
}) => {
|
|
56
|
-
const
|
|
57
|
-
const
|
|
60
|
+
const controlsVisible = useIslandControls(alwaysShowRun);
|
|
61
|
+
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
58
62
|
const selector = useCallback(
|
|
59
63
|
(s: NotebookState) => s.cellRuntime[cellId],
|
|
60
64
|
[cellId],
|
|
61
65
|
);
|
|
62
66
|
const runtime = useAtomValue(selectAtom(notebookAtom, selector));
|
|
63
67
|
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// oxlint-disable-next-line typescript/no-empty-function
|
|
69
|
-
alwaysShowRun ? () => {} : fn;
|
|
70
|
-
|
|
71
|
-
useEventListener(
|
|
72
|
-
document,
|
|
73
|
-
"keydown",
|
|
74
|
-
maybeNoop((e) => {
|
|
75
|
-
if (!alwaysShowRun && (e.metaKey || e.ctrlKey)) {
|
|
76
|
-
setPressed(true);
|
|
77
|
-
}
|
|
78
|
-
}),
|
|
79
|
-
);
|
|
80
|
-
useEventListener(
|
|
81
|
-
document,
|
|
82
|
-
"keyup",
|
|
83
|
-
maybeNoop((e) => {
|
|
84
|
-
if (
|
|
85
|
-
!alwaysShowRun &&
|
|
86
|
-
(e.metaKey || e.ctrlKey || e.key === "Meta" || e.key === "Control")
|
|
87
|
-
) {
|
|
88
|
-
setPressed(false);
|
|
89
|
-
}
|
|
90
|
-
}),
|
|
91
|
-
);
|
|
92
|
-
// Set pressed to false if the window loses focus
|
|
93
|
-
useEventListener(window, "blur", () => setPressed(false));
|
|
94
|
-
useEventListener(window, "mouseleave", () => setPressed(false));
|
|
68
|
+
// Sync cell status to the host <marimo-island> element as a data attribute
|
|
69
|
+
// so downstream consumers can style based on status (e.g. [data-status="running"])
|
|
70
|
+
const status = runtime?.status ?? "idle";
|
|
71
|
+
useSyncStatusToIsland(wrapperRef, status);
|
|
95
72
|
|
|
73
|
+
// If no runtime yet, show static content
|
|
96
74
|
if (!runtime?.output) {
|
|
97
|
-
return
|
|
75
|
+
return (
|
|
76
|
+
<div ref={wrapperRef} className="relative min-h-6 empty:hidden">
|
|
77
|
+
{children}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
98
80
|
}
|
|
99
81
|
|
|
100
82
|
// No output to display
|
|
101
|
-
// Maybe in future, we can configure this to
|
|
102
|
-
// fallback to displaying the code.
|
|
103
83
|
if (isOutputEmpty(runtime.output)) {
|
|
104
|
-
return
|
|
84
|
+
return <div ref={wrapperRef} />;
|
|
105
85
|
}
|
|
106
86
|
|
|
107
87
|
return (
|
|
108
|
-
<div className="relative min-h-6">
|
|
88
|
+
<div ref={wrapperRef} className="relative min-h-6">
|
|
109
89
|
<OutputRenderer message={runtime.output} />
|
|
110
|
-
<
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
tooltip="Copy code"
|
|
117
|
-
icon={<CopyIcon className="size-3" />}
|
|
118
|
-
action={() => copyToClipboard(codeCallback())}
|
|
119
|
-
/>
|
|
120
|
-
<IconButton
|
|
121
|
-
tooltip="Re-run cell"
|
|
122
|
-
icon={<PlayIcon className="size-3" />}
|
|
123
|
-
action={async () => {
|
|
124
|
-
await sendRun({
|
|
125
|
-
cellIds: [cellId],
|
|
126
|
-
codes: [codeCallback()],
|
|
127
|
-
}).catch((error) => {
|
|
128
|
-
Logger.error(error);
|
|
129
|
-
});
|
|
130
|
-
}}
|
|
131
|
-
/>
|
|
132
|
-
</div>
|
|
90
|
+
<RunningIndicator state={runtime} />
|
|
91
|
+
<IslandControls
|
|
92
|
+
cellId={cellId}
|
|
93
|
+
codeCallback={codeCallback}
|
|
94
|
+
visible={controlsVisible}
|
|
95
|
+
/>
|
|
133
96
|
</div>
|
|
134
97
|
);
|
|
135
98
|
};
|
|
136
99
|
|
|
137
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Sets `data-status` on the closest `<marimo-island>` ancestor whenever the
|
|
102
|
+
* cell's runtime status changes. This lets page authors style islands with
|
|
103
|
+
* CSS like `marimo-island[data-status="running"] { opacity: 0.5; }`.
|
|
104
|
+
*/
|
|
105
|
+
function useSyncStatusToIsland(
|
|
106
|
+
ref: React.RefObject<HTMLDivElement | null>,
|
|
107
|
+
status: string,
|
|
108
|
+
) {
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
const island = ref.current?.closest(ISLAND_TAG_NAMES.ISLAND);
|
|
111
|
+
island?.setAttribute("data-status", status);
|
|
112
|
+
}, [ref, status]);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Shows a spinning indicator when the cell is running
|
|
117
|
+
*/
|
|
118
|
+
const RunningIndicator: React.FC<{ state: CellRuntimeState }> = ({ state }) => {
|
|
138
119
|
if (state.status === "running") {
|
|
139
120
|
return (
|
|
140
121
|
<DelayRender>
|
|
@@ -148,7 +129,9 @@ const Indicator: React.FC<{ state: CellRuntimeState }> = ({ state }) => {
|
|
|
148
129
|
return null;
|
|
149
130
|
};
|
|
150
131
|
|
|
151
|
-
|
|
132
|
+
/**
|
|
133
|
+
* Delays rendering of children by 200ms using CSS animation
|
|
134
|
+
*/
|
|
152
135
|
const DelayRender: React.FC<PropsWithChildren> = ({ children }) => {
|
|
153
136
|
return <div className="animate-delayed-show-200">{children}</div>;
|
|
154
137
|
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useEventListener } from "@/hooks/useEventListener";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Hook to manage the visibility of island controls based on keyboard state.
|
|
8
|
+
*
|
|
9
|
+
* Controls are shown when:
|
|
10
|
+
* - alwaysShowRun is true, OR
|
|
11
|
+
* - User holds Cmd/Ctrl key
|
|
12
|
+
*
|
|
13
|
+
* @param alwaysShowRun - If true, controls are always visible
|
|
14
|
+
* @returns Whether controls should be shown
|
|
15
|
+
*/
|
|
16
|
+
export function useIslandControls(alwaysShowRun: boolean): boolean {
|
|
17
|
+
const [pressed, setPressed] = useState<boolean>(alwaysShowRun);
|
|
18
|
+
|
|
19
|
+
// No need to register if display is always on
|
|
20
|
+
const maybeNoop = <T>(fn: (e: T) => void) =>
|
|
21
|
+
// oxlint-disable-next-line typescript/no-empty-function
|
|
22
|
+
alwaysShowRun ? () => {} : fn;
|
|
23
|
+
|
|
24
|
+
useEventListener(
|
|
25
|
+
document,
|
|
26
|
+
"keydown",
|
|
27
|
+
maybeNoop((e: KeyboardEvent) => {
|
|
28
|
+
if (!alwaysShowRun && (e.metaKey || e.ctrlKey)) {
|
|
29
|
+
setPressed(true);
|
|
30
|
+
}
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
useEventListener(
|
|
35
|
+
document,
|
|
36
|
+
"keyup",
|
|
37
|
+
maybeNoop((e: KeyboardEvent) => {
|
|
38
|
+
if (
|
|
39
|
+
!alwaysShowRun &&
|
|
40
|
+
(e.metaKey || e.ctrlKey || e.key === "Meta" || e.key === "Control")
|
|
41
|
+
) {
|
|
42
|
+
setPressed(false);
|
|
43
|
+
}
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// Set pressed to false if the window loses focus
|
|
48
|
+
useEventListener(
|
|
49
|
+
window,
|
|
50
|
+
"blur",
|
|
51
|
+
maybeNoop(() => setPressed(false)),
|
|
52
|
+
);
|
|
53
|
+
useEventListener(
|
|
54
|
+
window,
|
|
55
|
+
"mouseleave",
|
|
56
|
+
maybeNoop(() => setPressed(false)),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
return pressed;
|
|
60
|
+
}
|