@marimo-team/frontend 0.23.1-dev8 → 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.
Files changed (69) hide show
  1. package/dist/assets/{JsonOutput-BY31ccA7.js → JsonOutput-CavtrueA.js} +1 -1
  2. package/dist/assets/{MarimoErrorOutput--Yd2Aw0J.js → MarimoErrorOutput-Bmp8DLLo.js} +1 -1
  3. package/dist/assets/RenderHTML-CM3WMmA8.js +1 -0
  4. package/dist/assets/{add-connection-dialog-CjvNOKgb.js → add-connection-dialog-BGZvJkor.js} +1 -1
  5. package/dist/assets/{agent-panel-C24uwabG.js → agent-panel-BvL9Lu9c.js} +1 -1
  6. package/dist/assets/{cell-editor-zW0u82sK.js → cell-editor-B40o_zx_.js} +1 -1
  7. package/dist/assets/{chat-display-DsHMZa9F.js → chat-display-M_nvYuHH.js} +1 -1
  8. package/dist/assets/{chat-panel-o9D3upnX.js → chat-panel-BMOW93uQ.js} +1 -1
  9. package/dist/assets/{chat-ui-BYS03y86.js → chat-ui-DyeimpVh.js} +1 -1
  10. package/dist/assets/{column-preview-Dwv5a_zE.js → column-preview-AfcgbFG_.js} +1 -1
  11. package/dist/assets/{command-palette-BYbKGSF3.js → command-palette-BgvdyU3B.js} +1 -1
  12. package/dist/assets/{documentation-panel-CA2pWMgB.js → documentation-panel-DUPcsi8P.js} +1 -1
  13. package/dist/assets/{edit-page-CMUN3ESy.js → edit-page-DD4uEDmX.js} +4 -4
  14. package/dist/assets/{error-panel-CbqfK1HJ.js → error-panel-DQOeSv5-.js} +1 -1
  15. package/dist/assets/{file-explorer-panel-CbS8z-JR.js → file-explorer-panel-B67zjs2X.js} +1 -1
  16. package/dist/assets/{form-DLyXacSF.js → form-BJ6VFU8l.js} +1 -1
  17. package/dist/assets/{hooks-kZJc1iBf.js → hooks-DvwShzDb.js} +1 -1
  18. package/dist/assets/index-y6osgSWB.js +42 -0
  19. package/dist/assets/{layout-tmN-U1zs.js → layout-erv8pLIP.js} +1 -1
  20. package/dist/assets/{panels-CLfdzLPR.js → panels-1u-RE72f.js} +1 -1
  21. package/dist/assets/{run-page-DPuH6QY4.js → run-page-DfWH_1mz.js} +1 -1
  22. package/dist/assets/{scratchpad-panel-BsMm0GQP.js → scratchpad-panel-CnaiXtoJ.js} +1 -1
  23. package/dist/assets/{session-panel-CTDzGShO.js → session-panel-C68GBFwH.js} +1 -1
  24. package/dist/assets/{snippets-panel-CWof0wHk.js → snippets-panel-BmIdR0lc.js} +1 -1
  25. package/dist/assets/state-D1n-olwf.js +3 -0
  26. package/dist/assets/{useNotebookActions-DHBEqrc_.js → useNotebookActions-Ch1o32Jw.js} +1 -1
  27. package/dist/index.html +7 -7
  28. package/package.json +4 -4
  29. package/src/core/islands/__tests__/bridge.test.ts +2 -12
  30. package/src/core/islands/__tests__/islands-harness.test.ts +348 -0
  31. package/src/core/islands/__tests__/parse.test.ts +466 -24
  32. package/src/core/islands/__tests__/test-utils.tsx +263 -0
  33. package/src/core/islands/bootstrap.ts +265 -0
  34. package/src/core/islands/bridge.ts +154 -75
  35. package/src/core/islands/components/IslandControls.tsx +103 -0
  36. package/src/core/islands/components/__tests__/IslandControls.test.tsx +185 -0
  37. package/src/core/islands/components/__tests__/useIslandControls.test.ts +208 -0
  38. package/src/core/islands/components/output-wrapper.tsx +76 -93
  39. package/src/core/islands/components/useIslandControls.ts +60 -0
  40. package/src/core/islands/components/web-components.tsx +168 -40
  41. package/src/core/islands/constants.ts +28 -0
  42. package/src/core/islands/main.ts +7 -205
  43. package/src/core/islands/parse.ts +73 -26
  44. package/src/core/islands/worker-factory.ts +86 -0
  45. package/src/plugins/core/RenderHTML.tsx +9 -0
  46. package/src/plugins/core/__test__/RenderHTML.test.ts +27 -0
  47. package/src/plugins/core/__test__/trusted-url.test.ts +48 -0
  48. package/src/plugins/core/registerReactComponent.tsx +11 -8
  49. package/src/plugins/core/trusted-url.ts +20 -0
  50. package/src/plugins/impl/ButtonPlugin.tsx +4 -6
  51. package/src/plugins/impl/CodeEditorPlugin.tsx +15 -18
  52. package/src/plugins/impl/DataEditorPlugin.tsx +8 -14
  53. package/src/plugins/impl/DataTablePlugin.tsx +8 -9
  54. package/src/plugins/impl/FileUploadPlugin.tsx +39 -43
  55. package/src/plugins/impl/FormPlugin.tsx +2 -6
  56. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +27 -1
  57. package/src/plugins/impl/anywidget/widget-binding.ts +13 -0
  58. package/src/plugins/impl/chat/ChatPlugin.tsx +17 -20
  59. package/src/plugins/impl/data-explorer/DataExplorerPlugin.tsx +5 -8
  60. package/src/plugins/impl/matplotlib/matplotlib-renderer.ts +38 -14
  61. package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +21 -0
  62. package/src/plugins/impl/mpl-interactive/__tests__/MplInteractivePlugin.test.tsx +119 -0
  63. package/src/plugins/impl/panel/PanelPlugin.tsx +31 -10
  64. package/src/plugins/impl/panel/__tests__/PanelPlugin.test.ts +60 -0
  65. package/src/plugins/impl/vega/VegaPlugin.tsx +5 -8
  66. package/src/plugins/layout/NavigationMenuPlugin.tsx +2 -6
  67. package/dist/assets/RenderHTML-CbuarQqA.js +0 -1
  68. package/dist/assets/index-bjxpaV0V.js +0 -42
  69. 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 { CopyIcon, Loader2Icon, PlayIcon } from "lucide-react";
5
+ import { Loader2Icon } from "lucide-react";
6
6
  import React, {
7
- type JSX,
8
7
  type PropsWithChildren,
9
8
  useCallback,
10
- useState,
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 { useRequestClient } from "@/core/network/requests";
20
- import { useEventListener } from "@/hooks/useEventListener";
21
- import { copyToClipboard } from "@/utils/copy";
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
- interface Props {
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
- interface IconButtonProps {
32
- tooltip: string;
33
- icon: JSX.Element;
34
- action: () => void;
40
+ /**
41
+ * Initial/static HTML content to display
42
+ */
43
+ children: React.ReactNode;
35
44
  }
36
45
 
37
- const IconButton: React.FC<IconButtonProps> = ({ tooltip, icon, action }) => (
38
- <Tooltip content={tooltip} delayDuration={200}>
39
- <Button
40
- size="icon"
41
- variant="outline"
42
- className="bg-background h-5 w-5 mb-0"
43
- onClick={action}
44
- >
45
- {icon}
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 { sendRun } = useRequestClient();
57
- const [pressed, setPressed] = useState<boolean>(alwaysShowRun);
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
- // No need to register, if display is default.
65
- // Lint still wants use to have the same event listeners per instance (which
66
- // makes sense), so noop is used.
67
- const maybeNoop = (fn: (e: KeyboardEvent) => void) =>
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 <div className="relative min-h-6 empty:hidden">{children}</div>;
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 null;
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
- <Indicator state={runtime} />
111
- <div
112
- className="absolute top-0 right-0 z-50 flex items-center justify-center gap-1"
113
- style={{ display: pressed ? "flex" : "none" }}
114
- >
115
- <IconButton
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
- const Indicator: React.FC<{ state: CellRuntimeState }> = ({ state }) => {
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
- // Render delay for children 200ms, using only css
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
+ }