@marimo-team/islands 0.23.15-dev4 → 0.23.15-dev40
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/{ConnectedDataExplorerComponent-CU4fZJzG.js → ConnectedDataExplorerComponent-7p7rt9iw.js} +4 -4
- package/dist/{ErrorBoundary-DE6tzZf-.js → ErrorBoundary-B_CAG7_e.js} +1 -1
- package/dist/{ImageComparisonComponent-CHrI72em.js → ImageComparisonComponent-1i5IBhon.js} +173 -129
- package/dist/assets/__vite-browser-external-CjNAy01L.js +1 -0
- package/dist/assets/worker-B3XPCb6y.js +98 -0
- package/dist/{chat-ui-DinOvbWu.js → chat-ui-C0z13gJB.js} +3111 -3111
- package/dist/{code-visibility-Dk8cVOny.js → code-visibility-CSsHnVZu.js} +946 -864
- package/dist/{constants-T20xxyNf.js → debounce-BOD3DbfP.js} +1 -24
- package/dist/{formats-Dzx4J_z1.js → formats-CitsMtUm.js} +1 -1
- package/dist/{glide-data-editor-CjTu7ukN.js → glide-data-editor-CwZz71BD.js} +3 -3
- package/dist/{html-to-image-_wGfk8V-.js → html-to-image-CEo5pRYw.js} +2291 -2250
- package/dist/{input-DtsN7xm-.js → input-BGPrFH3g.js} +2 -2
- package/dist/main.js +1297 -1201
- package/dist/{mermaid-BYqXy_NE.js → mermaid-UdmxG2PZ.js} +2 -2
- package/dist/{process-output-Mh4UrjwM.js → process-output-BOvvilRG.js} +1 -1
- package/dist/{reveal-component-B5DXLyO7.js → reveal-component-DGjJVNMQ.js} +618 -605
- package/dist/{spec-Cz-Bj1JI.js → spec-DSs9v0xx.js} +1 -1
- package/dist/style.css +1 -1
- package/dist/{toDate-CWNNlFEX.js → toDate-BRJgtOGm.js} +14 -5
- package/dist/{useAsyncData-KfHB8wQR.js → useAsyncData-BSOyAbac.js} +1 -1
- package/dist/{useDeepCompareMemoize-nJMtxhm4.js → useDeepCompareMemoize-BTLeWzuR.js} +1 -1
- package/dist/{useLifecycle-DegSo0lV.js → useLifecycle-C6wHjkhW.js} +1 -1
- package/dist/{useTheme-6eZ3GOTS.js → useTheme-Df_vGflw.js} +62 -27
- package/dist/{vega-component-DzyyM9fc.js → vega-component-BAvmvTjO.js} +6 -6
- package/package.json +1 -1
- package/src/components/chat/__tests__/chat-utils.test.ts +244 -1
- package/src/components/chat/__tests__/message-queue.test.tsx +121 -0
- package/src/components/chat/chat-panel.tsx +196 -67
- package/src/components/chat/chat-utils.ts +111 -2
- package/src/components/editor/SortableCell.tsx +6 -2
- package/src/components/editor/__tests__/output-persistence.test.tsx +241 -0
- package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +153 -0
- package/src/components/editor/actions/pair-with-agent-commands.ts +109 -0
- package/src/components/editor/actions/pair-with-agent-modal.tsx +20 -64
- package/src/components/editor/ai/add-cell-with-ai.tsx +14 -5
- package/src/components/editor/ai/ai-completion-editor.tsx +4 -1
- package/src/components/editor/cell/cell-context-menu.tsx +7 -0
- package/src/components/editor/chrome/panels/packages-panel.tsx +11 -1
- package/src/components/editor/columns/__tests__/cell-column.test.tsx +105 -0
- package/src/components/editor/columns/cell-column.tsx +34 -4
- package/src/components/editor/columns/sortable-column.tsx +24 -4
- package/src/components/editor/file-tree/download.ts +46 -0
- package/src/components/editor/file-tree/file-explorer.tsx +3 -21
- package/src/components/editor/file-tree/file-viewer.tsx +4 -27
- package/src/components/editor/navigation/__tests__/navigation.test.ts +33 -0
- package/src/components/editor/navigation/navigation.ts +8 -1
- package/src/components/editor/notebook-cell.tsx +203 -106
- package/src/components/editor/renderers/__tests__/cells-renderer.test.tsx +66 -0
- package/src/components/editor/renderers/cell-array.tsx +26 -13
- package/src/components/editor/renderers/cells-renderer.tsx +8 -2
- package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +29 -0
- package/src/components/editor/renderers/slides-layout/types.ts +4 -0
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +19 -19
- package/src/components/slides/reveal-component.tsx +37 -4
- package/src/components/slides/slide-form.tsx +72 -0
- package/src/components/ui/toast.tsx +11 -3
- package/src/components/ui/toaster.tsx +65 -9
- package/src/core/cells/__tests__/utils.test.ts +59 -0
- package/src/core/cells/utils.ts +51 -0
- package/src/core/codemirror/completion/__tests__/signature-hint.test.ts +97 -3
- package/src/core/codemirror/completion/signature-hint.ts +78 -11
- package/src/core/constants.ts +6 -0
- package/src/core/islands/__tests__/bridge.test.ts +341 -46
- package/src/core/islands/__tests__/main.test.ts +176 -0
- package/src/core/islands/__tests__/parse.test.ts +105 -0
- package/src/core/islands/bootstrap.ts +8 -3
- package/src/core/islands/bridge.ts +116 -23
- package/src/core/islands/components/__tests__/web-components.test.tsx +214 -0
- package/src/core/islands/components/web-components.tsx +76 -15
- package/src/core/islands/constants.ts +1 -0
- package/src/core/islands/main.ts +69 -3
- package/src/core/islands/parse.ts +70 -23
- package/src/core/islands/worker/__tests__/controller.test.ts +173 -0
- package/src/core/islands/worker/worker.tsx +145 -57
- package/src/core/runtime/__tests__/runtime.test.ts +64 -0
- package/src/core/runtime/runtime.ts +30 -10
- package/src/core/wasm/worker/bootstrap.ts +113 -20
- package/src/css/app/Cell.css +10 -0
- package/src/hooks/__tests__/useHotkey.test.tsx +88 -0
- package/src/hooks/useHotkey.ts +29 -4
- package/src/plugins/impl/anywidget/__tests__/host.test.ts +6 -9
- package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +22 -61
- package/src/plugins/impl/anywidget/model-proxy.ts +0 -13
- package/src/plugins/impl/anywidget/widget-binding.ts +4 -34
- package/src/plugins/impl/image-comparison/ImageComparisonComponent.tsx +53 -2
- package/src/plugins/impl/image-comparison/__tests__/ImageComparisonComponent.test.tsx +71 -0
- package/src/plugins/impl/matplotlib/__tests__/matplotlib-renderer.test.ts +71 -3
- package/src/plugins/impl/matplotlib/matplotlib-renderer.ts +1 -0
- package/src/theme/__tests__/useTheme.test.ts +68 -0
- package/src/theme/useTheme.ts +16 -1
- package/dist/assets/__vite-browser-external-BQCLNwri.js +0 -1
- package/dist/assets/worker-DAWRHcPq.js +0 -73
|
@@ -39,12 +39,18 @@ interface Props extends Pick<
|
|
|
39
39
|
"cellId" | "getEditorView"
|
|
40
40
|
> {
|
|
41
41
|
children: React.ReactNode;
|
|
42
|
+
/**
|
|
43
|
+
* If true, the custom context menu is disabled and the native one is used
|
|
44
|
+
* (e.g. while presenting).
|
|
45
|
+
*/
|
|
46
|
+
disabled?: boolean;
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
export const CellActionsContextMenu = ({
|
|
45
50
|
children,
|
|
46
51
|
cellId,
|
|
47
52
|
getEditorView,
|
|
53
|
+
disabled,
|
|
48
54
|
}: Props) => {
|
|
49
55
|
const cellData = useCellData(cellId);
|
|
50
56
|
const cellRuntime = useCellRuntime(cellId);
|
|
@@ -187,6 +193,7 @@ export const CellActionsContextMenu = ({
|
|
|
187
193
|
return (
|
|
188
194
|
<ContextMenu>
|
|
189
195
|
<ContextMenuTrigger
|
|
196
|
+
disabled={disabled}
|
|
190
197
|
onContextMenu={(evt) => {
|
|
191
198
|
if (evt.target instanceof HTMLImageElement) {
|
|
192
199
|
setImageRightClicked(evt.target);
|
|
@@ -303,6 +303,16 @@ const PackagesList: React.FC<{
|
|
|
303
303
|
onSuccess: () => void;
|
|
304
304
|
packages: { name: string; version: string }[];
|
|
305
305
|
}> = ({ onSuccess, packages }) => {
|
|
306
|
+
// Sort case-insensitively so packages are strictly alphabetical
|
|
307
|
+
// regardless of capitalization (package managers sort inconsistently).
|
|
308
|
+
const sortedPackages = React.useMemo(
|
|
309
|
+
() =>
|
|
310
|
+
packages.toSorted((a, b) =>
|
|
311
|
+
a.name.localeCompare(b.name, undefined, { sensitivity: "base" }),
|
|
312
|
+
),
|
|
313
|
+
[packages],
|
|
314
|
+
);
|
|
315
|
+
|
|
306
316
|
if (packages.length === 0) {
|
|
307
317
|
return (
|
|
308
318
|
<PanelEmptyState
|
|
@@ -323,7 +333,7 @@ const PackagesList: React.FC<{
|
|
|
323
333
|
</TableRow>
|
|
324
334
|
</TableHeader>
|
|
325
335
|
<TableBody>
|
|
326
|
-
{
|
|
336
|
+
{sortedPackages.map((item) => (
|
|
327
337
|
<TableRow
|
|
328
338
|
key={item.name}
|
|
329
339
|
className="group"
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import { createStore, Provider } from "jotai";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
6
|
+
import type { CellColumnId } from "@/utils/id-tree";
|
|
7
|
+
import { Column } from "../cell-column";
|
|
8
|
+
|
|
9
|
+
const columnId = "col-1" as CellColumnId;
|
|
10
|
+
|
|
11
|
+
function renderColumn({
|
|
12
|
+
width,
|
|
13
|
+
presenting,
|
|
14
|
+
}: {
|
|
15
|
+
width: "compact" | "columns";
|
|
16
|
+
presenting: boolean;
|
|
17
|
+
}) {
|
|
18
|
+
const store = createStore();
|
|
19
|
+
return render(
|
|
20
|
+
<Provider store={store}>
|
|
21
|
+
<TooltipProvider>
|
|
22
|
+
<Column
|
|
23
|
+
columnId={columnId}
|
|
24
|
+
index={0}
|
|
25
|
+
width={width}
|
|
26
|
+
canDelete={false}
|
|
27
|
+
canMoveLeft={false}
|
|
28
|
+
canMoveRight={false}
|
|
29
|
+
presenting={presenting}
|
|
30
|
+
>
|
|
31
|
+
<div data-testid="column-children" />
|
|
32
|
+
</Column>
|
|
33
|
+
</TooltipProvider>
|
|
34
|
+
</Provider>,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("Column (single-column notebook)", () => {
|
|
39
|
+
it("spaces cells with the --notebook-cell-gap variable", () => {
|
|
40
|
+
const { getByTestId } = renderColumn({
|
|
41
|
+
width: "compact",
|
|
42
|
+
presenting: false,
|
|
43
|
+
});
|
|
44
|
+
const column = getByTestId("cell-column");
|
|
45
|
+
expect(column.classList.contains("gap-(--notebook-cell-gap)")).toBe(true);
|
|
46
|
+
expect(column.classList.contains("[--notebook-cell-gap:0px]")).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("zeroes the cell gap while presenting", () => {
|
|
50
|
+
const { getByTestId } = renderColumn({
|
|
51
|
+
width: "compact",
|
|
52
|
+
presenting: true,
|
|
53
|
+
});
|
|
54
|
+
const column = getByTestId("cell-column");
|
|
55
|
+
expect(column.classList.contains("gap-(--notebook-cell-gap)")).toBe(true);
|
|
56
|
+
expect(column.classList.contains("[--notebook-cell-gap:0px]")).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("Column (multi-column notebook)", () => {
|
|
61
|
+
it("shows column chrome when not presenting", () => {
|
|
62
|
+
const { getByTestId, getAllByTestId } = renderColumn({
|
|
63
|
+
width: "columns",
|
|
64
|
+
presenting: false,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(getByTestId("column-header").hidden).toBe(false);
|
|
68
|
+
expect(getByTestId("column-frame").classList.contains("border")).toBe(true);
|
|
69
|
+
for (const handle of getAllByTestId("column-resize-handle")) {
|
|
70
|
+
expect(handle.hidden).toBe(false);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("hides column chrome while presenting, without unmounting it", () => {
|
|
75
|
+
const { getByTestId, getAllByTestId } = renderColumn({
|
|
76
|
+
width: "columns",
|
|
77
|
+
presenting: true,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Header (drag handle, move/delete/add buttons) is hidden but mounted
|
|
81
|
+
expect(getByTestId("column-header").hidden).toBe(true);
|
|
82
|
+
// No border around the column
|
|
83
|
+
expect(getByTestId("column-frame").classList.contains("border")).toBe(
|
|
84
|
+
false,
|
|
85
|
+
);
|
|
86
|
+
// Resize handles are hidden
|
|
87
|
+
const handles = getAllByTestId("column-resize-handle");
|
|
88
|
+
expect(handles.length).toBeGreaterThan(0);
|
|
89
|
+
for (const handle of handles) {
|
|
90
|
+
expect(handle.hidden).toBe(true);
|
|
91
|
+
}
|
|
92
|
+
// Cell gap is zeroed on the column root (cascades to descendants)
|
|
93
|
+
const columnRoot = getByTestId("column-frame").parentElement;
|
|
94
|
+
expect(columnRoot?.classList.contains("[--notebook-cell-gap:0px]")).toBe(
|
|
95
|
+
true,
|
|
96
|
+
);
|
|
97
|
+
// Editor geometry (saved widths, min-width, gutters) is replaced by the
|
|
98
|
+
// published content width
|
|
99
|
+
const inner = getByTestId("column-children").parentElement;
|
|
100
|
+
expect(inner?.classList.contains("min-w-[500px]")).toBe(false);
|
|
101
|
+
expect(inner?.classList.contains("w-(--content-width)")).toBe(true);
|
|
102
|
+
// Children stay mounted
|
|
103
|
+
expect(getByTestId("column-children")).toBeTruthy();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import React, { memo, useRef } from "react";
|
|
4
4
|
import type { AppConfig } from "@/core/config/config-schema";
|
|
5
5
|
import { useResizeHandle } from "@/hooks/useResizeHandle";
|
|
6
|
+
import { cn } from "@/utils/cn";
|
|
6
7
|
import type { CellColumnId } from "@/utils/id-tree";
|
|
7
8
|
import { SortableColumn } from "./sortable-column";
|
|
8
9
|
import { storageFn } from "./storage";
|
|
@@ -17,6 +18,11 @@ interface Props {
|
|
|
17
18
|
canDelete: boolean;
|
|
18
19
|
canMoveLeft: boolean;
|
|
19
20
|
canMoveRight: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* If true, column chrome (drag handles, resize handles, borders) is hidden
|
|
23
|
+
* while keeping the same component tree mounted.
|
|
24
|
+
*/
|
|
25
|
+
presenting?: boolean;
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
const { getColumnWidth, saveColumnWidth } = storageFn;
|
|
@@ -24,6 +30,10 @@ const { getColumnWidth, saveColumnWidth } = storageFn;
|
|
|
24
30
|
export const Column = memo((props: Props) => {
|
|
25
31
|
const columnRef = useRef<HTMLDivElement>(null);
|
|
26
32
|
|
|
33
|
+
// Published cells are not spaced apart. Set once on the column root; the
|
|
34
|
+
// CSS variable cascades to the gap-using descendants.
|
|
35
|
+
const zeroGapWhenPresenting = props.presenting && "[--notebook-cell-gap:0px]";
|
|
36
|
+
|
|
27
37
|
if (props.width === "columns") {
|
|
28
38
|
return (
|
|
29
39
|
<SortableColumn
|
|
@@ -33,14 +43,16 @@ export const Column = memo((props: Props) => {
|
|
|
33
43
|
columnId={props.columnId}
|
|
34
44
|
canMoveLeft={props.canMoveLeft}
|
|
35
45
|
canMoveRight={props.canMoveRight}
|
|
36
|
-
className="group/column"
|
|
46
|
+
className={cn("group/column", zeroGapWhenPresenting)}
|
|
37
47
|
footer={props.footer}
|
|
48
|
+
presenting={props.presenting}
|
|
38
49
|
>
|
|
39
50
|
<ResizableComponent
|
|
40
51
|
startingWidth={getColumnWidth(props.index)}
|
|
41
52
|
onResize={(width: number) => {
|
|
42
53
|
saveColumnWidth(props.index, width);
|
|
43
54
|
}}
|
|
55
|
+
presenting={props.presenting}
|
|
44
56
|
>
|
|
45
57
|
{props.children}
|
|
46
58
|
</ResizableComponent>
|
|
@@ -50,7 +62,13 @@ export const Column = memo((props: Props) => {
|
|
|
50
62
|
|
|
51
63
|
return (
|
|
52
64
|
<>
|
|
53
|
-
<div
|
|
65
|
+
<div
|
|
66
|
+
data-testid="cell-column"
|
|
67
|
+
className={cn(
|
|
68
|
+
"flex flex-col gap-(--notebook-cell-gap)",
|
|
69
|
+
zeroGapWhenPresenting,
|
|
70
|
+
)}
|
|
71
|
+
>
|
|
54
72
|
{props.children}
|
|
55
73
|
</div>
|
|
56
74
|
{props.footer}
|
|
@@ -64,12 +82,14 @@ interface ResizableComponentProps {
|
|
|
64
82
|
startingWidth: number | "contentWidth";
|
|
65
83
|
onResize?: (width: number) => void;
|
|
66
84
|
children: React.ReactNode;
|
|
85
|
+
presenting?: boolean;
|
|
67
86
|
}
|
|
68
87
|
|
|
69
88
|
const ResizableComponent = ({
|
|
70
89
|
startingWidth,
|
|
71
90
|
onResize,
|
|
72
91
|
children,
|
|
92
|
+
presenting,
|
|
73
93
|
}: ResizableComponentProps) => {
|
|
74
94
|
const { resizableDivRef, handleRefs, style } = useResizeHandle({
|
|
75
95
|
startingWidth,
|
|
@@ -80,11 +100,13 @@ const ResizableComponent = ({
|
|
|
80
100
|
return (
|
|
81
101
|
<div
|
|
82
102
|
ref={ref}
|
|
103
|
+
data-testid="column-resize-handle"
|
|
83
104
|
className={`w-[3px] cursor-col-resize transition-colors duration-200 z-100
|
|
84
105
|
relative before:content-[''] before:absolute before:inset-y-0 before:left-[-3px]
|
|
85
106
|
before:right-[-3px] before:w-[9px] before:z-[-1]
|
|
86
107
|
hover/column:bg-[var(--slate-3)] dark:hover/column:bg-[var(--slate-5)]
|
|
87
108
|
hover/column:hover:bg-primary/60 dark:hover/column:hover:bg-primary/60`}
|
|
109
|
+
hidden={presenting}
|
|
88
110
|
/>
|
|
89
111
|
);
|
|
90
112
|
};
|
|
@@ -94,8 +116,16 @@ const ResizableComponent = ({
|
|
|
94
116
|
{renderResizeHandler(handleRefs.left)}
|
|
95
117
|
<div
|
|
96
118
|
ref={resizableDivRef}
|
|
97
|
-
className=
|
|
98
|
-
|
|
119
|
+
className={cn(
|
|
120
|
+
"flex flex-col gap-(--notebook-cell-gap) box-content z-1",
|
|
121
|
+
// While presenting, use the published content width instead of the
|
|
122
|
+
// editor geometry (saved pixel width, 500px minimum, gutters),
|
|
123
|
+
// which overflows narrow screens.
|
|
124
|
+
presenting
|
|
125
|
+
? "w-(--content-width)"
|
|
126
|
+
: "min-h-[100px] px-11 pt-3 pb-6 min-w-[500px]",
|
|
127
|
+
)}
|
|
128
|
+
style={presenting ? undefined : style}
|
|
99
129
|
>
|
|
100
130
|
{children}
|
|
101
131
|
</div>
|
|
@@ -30,11 +30,24 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
30
30
|
canMoveLeft: boolean;
|
|
31
31
|
canMoveRight: boolean;
|
|
32
32
|
footer?: React.ReactNode;
|
|
33
|
+
/**
|
|
34
|
+
* If true, dragging is disabled and the column chrome is hidden
|
|
35
|
+
* (e.g. while presenting).
|
|
36
|
+
*/
|
|
37
|
+
presenting?: boolean;
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
const SortableColumnInternal = React.forwardRef(
|
|
36
41
|
(
|
|
37
|
-
{
|
|
42
|
+
{
|
|
43
|
+
columnId,
|
|
44
|
+
canDelete,
|
|
45
|
+
canMoveLeft,
|
|
46
|
+
canMoveRight,
|
|
47
|
+
footer,
|
|
48
|
+
presenting,
|
|
49
|
+
...props
|
|
50
|
+
}: Props,
|
|
38
51
|
ref: React.Ref<HTMLDivElement>,
|
|
39
52
|
) => {
|
|
40
53
|
const {
|
|
@@ -45,7 +58,7 @@ const SortableColumnInternal = React.forwardRef(
|
|
|
45
58
|
transition,
|
|
46
59
|
isDragging,
|
|
47
60
|
isOver,
|
|
48
|
-
} = useSortable({ id: columnId });
|
|
61
|
+
} = useSortable({ id: columnId, disabled: presenting });
|
|
49
62
|
|
|
50
63
|
const style: React.CSSProperties = {
|
|
51
64
|
transform: transform
|
|
@@ -78,7 +91,11 @@ const SortableColumnInternal = React.forwardRef(
|
|
|
78
91
|
};
|
|
79
92
|
|
|
80
93
|
const dragHandle = (
|
|
81
|
-
<div
|
|
94
|
+
<div
|
|
95
|
+
data-testid="column-header"
|
|
96
|
+
className="px-2 pb-0 group flex items-center overflow-hidden border-b border-(--slate-7)"
|
|
97
|
+
hidden={presenting}
|
|
98
|
+
>
|
|
82
99
|
<Tooltip content="Move column left" side="top" delayDuration={300}>
|
|
83
100
|
<Button
|
|
84
101
|
variant="text"
|
|
@@ -161,7 +178,10 @@ const SortableColumnInternal = React.forwardRef(
|
|
|
161
178
|
isOver && "bg-accent/20", // Add a background color when dragging over
|
|
162
179
|
)}
|
|
163
180
|
>
|
|
164
|
-
<div
|
|
181
|
+
<div
|
|
182
|
+
data-testid="column-frame"
|
|
183
|
+
className={cn(!presenting && "border border-(--slate-7)")}
|
|
184
|
+
>
|
|
165
185
|
{dragHandle}
|
|
166
186
|
{props.children}
|
|
167
187
|
</div>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { getRequestClient } from "@/core/network/requests";
|
|
4
|
+
import { getRuntimeManager } from "@/core/runtime/config";
|
|
5
|
+
import { isWasm } from "@/core/wasm/utils";
|
|
6
|
+
import { deserializeBlob } from "@/utils/blob";
|
|
7
|
+
import { downloadBlob, downloadByURL } from "@/utils/download";
|
|
8
|
+
import { type Base64String, base64ToDataURL } from "@/utils/json/base64";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Download a workspace file by path.
|
|
12
|
+
*
|
|
13
|
+
* Over HTTP this navigates to a streaming download endpoint, so the
|
|
14
|
+
* browser's download manager handles the transfer and no file contents
|
|
15
|
+
* are buffered in memory. In WASM there is no HTTP server to stream
|
|
16
|
+
* from; contents come over the pyodide bridge as base64 and are saved
|
|
17
|
+
* via a Blob.
|
|
18
|
+
*/
|
|
19
|
+
export async function downloadFile(path: string, name: string): Promise<void> {
|
|
20
|
+
if (!isWasm()) {
|
|
21
|
+
const url = getRuntimeManager().formatNavigableHttpURL(
|
|
22
|
+
"api/files/download",
|
|
23
|
+
new URLSearchParams({ path }),
|
|
24
|
+
);
|
|
25
|
+
downloadByURL(url.toString(), name);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const details = await getRequestClient().sendFileDetails({ path });
|
|
30
|
+
if (details.isBase64 && details.contents) {
|
|
31
|
+
const blob = deserializeBlob(
|
|
32
|
+
base64ToDataURL(
|
|
33
|
+
details.contents as Base64String,
|
|
34
|
+
details.mimeType || "application/octet-stream",
|
|
35
|
+
),
|
|
36
|
+
);
|
|
37
|
+
downloadBlob(blob, name);
|
|
38
|
+
} else {
|
|
39
|
+
downloadBlob(
|
|
40
|
+
new Blob([details.contents || ""], {
|
|
41
|
+
type: details.mimeType || "application/octet-stream",
|
|
42
|
+
}),
|
|
43
|
+
name,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -62,16 +62,14 @@ import type { FileInfo } from "@/core/network/types";
|
|
|
62
62
|
import { isWasm } from "@/core/wasm/utils";
|
|
63
63
|
import { useAsyncData } from "@/hooks/useAsyncData";
|
|
64
64
|
import { ErrorBanner } from "@/plugins/impl/common/error-banner";
|
|
65
|
-
import { deserializeBlob } from "@/utils/blob";
|
|
66
65
|
import { cn } from "@/utils/cn";
|
|
67
66
|
import { copyToClipboard } from "@/utils/copy";
|
|
68
|
-
import { downloadBlob } from "@/utils/download";
|
|
69
|
-
import { type Base64String, base64ToDataURL } from "@/utils/json/base64";
|
|
70
67
|
import { openNotebook } from "@/utils/links";
|
|
71
68
|
import type { FilePath } from "@/utils/paths";
|
|
72
69
|
import { makeDuplicateName } from "@/utils/pathUtils";
|
|
73
70
|
import { jotaiJsonStorage } from "@/utils/storage/jotai";
|
|
74
71
|
import { useTreeDndManager } from "./dnd-wrapper";
|
|
72
|
+
import { downloadFile } from "./download";
|
|
75
73
|
import { FileViewer } from "./file-viewer";
|
|
76
74
|
import type { RequestingTree } from "./requesting-tree";
|
|
77
75
|
import { openStateAtom, treeAtom } from "./state";
|
|
@@ -393,7 +391,7 @@ const Show = ({
|
|
|
393
391
|
};
|
|
394
392
|
|
|
395
393
|
const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
|
|
396
|
-
const { openFile
|
|
394
|
+
const { openFile } = useRequestClient();
|
|
397
395
|
const disableFileDownloads = useAtomValue(disableFileDownloadsAtom);
|
|
398
396
|
|
|
399
397
|
const fileType: FileIconType = node.data.isDirectory
|
|
@@ -645,23 +643,7 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
|
|
|
645
643
|
<>
|
|
646
644
|
<DropdownMenuItem
|
|
647
645
|
onSelect={async () => {
|
|
648
|
-
|
|
649
|
-
path: node.data.path,
|
|
650
|
-
});
|
|
651
|
-
if (details.isBase64 && details.contents) {
|
|
652
|
-
const blob = deserializeBlob(
|
|
653
|
-
base64ToDataURL(
|
|
654
|
-
details.contents as Base64String,
|
|
655
|
-
details.mimeType || "application/octet-stream",
|
|
656
|
-
),
|
|
657
|
-
);
|
|
658
|
-
downloadBlob(blob, node.data.name);
|
|
659
|
-
} else {
|
|
660
|
-
downloadBlob(
|
|
661
|
-
new Blob([details.contents || ""]),
|
|
662
|
-
node.data.name,
|
|
663
|
-
);
|
|
664
|
-
}
|
|
646
|
+
await downloadFile(node.data.path, node.data.name);
|
|
665
647
|
}}
|
|
666
648
|
data-testid="file-explorer-download-menu-item"
|
|
667
649
|
>
|
|
@@ -21,10 +21,9 @@ import { filenameAtom } from "@/core/saving/file-state";
|
|
|
21
21
|
import { isWasm } from "@/core/wasm/utils";
|
|
22
22
|
import { useAsyncData } from "@/hooks/useAsyncData";
|
|
23
23
|
import { ErrorBanner } from "@/plugins/impl/common/error-banner";
|
|
24
|
-
import { deserializeBlob } from "@/utils/blob";
|
|
25
24
|
import { copyToClipboard } from "@/utils/copy";
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
25
|
+
import type { Base64String } from "@/utils/json/base64";
|
|
26
|
+
import { downloadFile } from "./download";
|
|
28
27
|
import { FilePreviewHeader } from "./file-header";
|
|
29
28
|
import {
|
|
30
29
|
FileContentRenderer,
|
|
@@ -121,30 +120,8 @@ export const FileViewer: React.FC<Props> = ({ file, onOpenNotebook }) => {
|
|
|
121
120
|
);
|
|
122
121
|
}
|
|
123
122
|
|
|
124
|
-
const handleDownload = () => {
|
|
125
|
-
|
|
126
|
-
if (isMediaMime(mimeType)) {
|
|
127
|
-
const dataURL = base64ToDataURL(
|
|
128
|
-
data.contents as Base64String,
|
|
129
|
-
mimeType,
|
|
130
|
-
);
|
|
131
|
-
downloadByURL(dataURL, data.file.name);
|
|
132
|
-
} else {
|
|
133
|
-
const blob = deserializeBlob(
|
|
134
|
-
base64ToDataURL(
|
|
135
|
-
data.contents as Base64String,
|
|
136
|
-
data.mimeType || "application/octet-stream",
|
|
137
|
-
),
|
|
138
|
-
);
|
|
139
|
-
downloadBlob(blob, data.file.name);
|
|
140
|
-
}
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
downloadBlob(
|
|
145
|
-
new Blob([data.contents || internalValue], { type: mimeType }),
|
|
146
|
-
data.file.name,
|
|
147
|
-
);
|
|
123
|
+
const handleDownload = async () => {
|
|
124
|
+
await downloadFile(file.path, data.file.name);
|
|
148
125
|
};
|
|
149
126
|
|
|
150
127
|
const header = (
|
|
@@ -12,6 +12,7 @@ import { MockRequestClient } from "@/__mocks__/requests";
|
|
|
12
12
|
import { aiCompletionCellAtom } from "@/core/ai/state";
|
|
13
13
|
import type { CellActions } from "@/core/cells/cells";
|
|
14
14
|
import { notebookAtom } from "@/core/cells/cells";
|
|
15
|
+
import { signatureHintField } from "@/core/codemirror/completion/signature-hint";
|
|
15
16
|
import {
|
|
16
17
|
configOverridesAtom,
|
|
17
18
|
platformAtom,
|
|
@@ -1825,6 +1826,38 @@ describe("useCellEditorNavigationProps", () => {
|
|
|
1825
1826
|
expect(focusCell).not.toHaveBeenCalled();
|
|
1826
1827
|
});
|
|
1827
1828
|
|
|
1829
|
+
it("should dismiss the signature hint when Escape is pressed without exiting to command mode", () => {
|
|
1830
|
+
mockSimplifySelection.mockReturnValue(false);
|
|
1831
|
+
mockCompletionStatus.mockReturnValue(null);
|
|
1832
|
+
|
|
1833
|
+
const dispatch = vi.fn();
|
|
1834
|
+
const mockEditorView = {
|
|
1835
|
+
current: {
|
|
1836
|
+
state: {
|
|
1837
|
+
selection: { main: { from: 9, to: 9, empty: true } },
|
|
1838
|
+
field: vi.fn((field: unknown) =>
|
|
1839
|
+
field === signatureHintField ? { pos: 9 } : false,
|
|
1840
|
+
),
|
|
1841
|
+
},
|
|
1842
|
+
dispatch,
|
|
1843
|
+
} as unknown as EditorView,
|
|
1844
|
+
};
|
|
1845
|
+
|
|
1846
|
+
const { result } = renderWithProvider(() =>
|
|
1847
|
+
useCellEditorNavigationProps(mockCellId, mockEditorView),
|
|
1848
|
+
);
|
|
1849
|
+
|
|
1850
|
+
const mockEvent = Mocks.keyboardEvent({ key: "Escape" });
|
|
1851
|
+
|
|
1852
|
+
act(() => {
|
|
1853
|
+
result.current.onKeyDown?.(mockEvent);
|
|
1854
|
+
});
|
|
1855
|
+
|
|
1856
|
+
// The hint is dismissed (dispatch called) but we stay in edit mode.
|
|
1857
|
+
expect(dispatch).toHaveBeenCalled();
|
|
1858
|
+
expect(focusCell).not.toHaveBeenCalled();
|
|
1859
|
+
});
|
|
1860
|
+
|
|
1828
1861
|
it("should continue propagation for other keys", () => {
|
|
1829
1862
|
const mockEditorView = { current: null };
|
|
1830
1863
|
const { result } = renderWithProvider(() =>
|
|
@@ -22,6 +22,10 @@ import {
|
|
|
22
22
|
} from "@/core/cells/pending-cut-service";
|
|
23
23
|
import { usePendingDeleteService } from "@/core/cells/pending-delete-service";
|
|
24
24
|
import { scrollCellIntoView } from "@/core/cells/scrollCellIntoView";
|
|
25
|
+
import {
|
|
26
|
+
closeSignatureHint,
|
|
27
|
+
signatureHintField,
|
|
28
|
+
} from "@/core/codemirror/completion/signature-hint";
|
|
25
29
|
import {
|
|
26
30
|
hotkeysAtom,
|
|
27
31
|
isAiFeatureEnabled,
|
|
@@ -713,7 +717,9 @@ export function useCellEditorNavigationProps(
|
|
|
713
717
|
return;
|
|
714
718
|
}
|
|
715
719
|
|
|
716
|
-
const hasSignatureHelp =
|
|
720
|
+
const hasSignatureHelp =
|
|
721
|
+
Boolean(state.field(signatureHelpTooltipField, false)) ||
|
|
722
|
+
Boolean(state.field(signatureHintField, false));
|
|
717
723
|
const hasAutocompletePopup = completionStatus(state) !== null;
|
|
718
724
|
if (hasSignatureHelp) {
|
|
719
725
|
closeSignatureHelp(view);
|
|
@@ -784,4 +790,5 @@ export function closeSignatureHelp(view: EditorView) {
|
|
|
784
790
|
if (view.state.field(signatureHelpTooltipField, false)) {
|
|
785
791
|
view.dispatch({ effects: setSignatureHelpTooltip.of(null) });
|
|
786
792
|
}
|
|
793
|
+
closeSignatureHint(view);
|
|
787
794
|
}
|