@marimo-team/islands 0.24.1-dev87 → 0.24.1-dev90
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/{chat-ui-DpE4Aa_M.js → chat-ui-BqDoKana.js} +5 -5
- package/dist/{common-Csjs6-Cq.js → common-FuL_5-mY.js} +14 -14
- package/dist/{formats-CMBaNtI6.js → formats-nPPeLOk2.js} +1 -1
- package/dist/{html-to-image-LGo891lC.js → html-to-image-BMTAw0YV.js} +3579 -3580
- package/dist/main.js +5830 -5816
- package/dist/{process-output-BMotjkzK.js → process-output-JALCTHOK.js} +1 -1
- package/dist/{reveal-component-t0AuZBu7.js → reveal-component-e4yYomxA.js} +2 -2
- package/dist/{toDate-BbnMXiIl.js → toDate-DfFzRDkN.js} +1 -1
- package/dist/{vega-component-Cid89pdp.js → vega-component-PYsP9Jod.js} +11 -11
- package/package.json +1 -1
- package/src/components/editor/output/__tests__/ansi-reduce.test.ts +36 -10
- package/src/components/editor/output/ansi-reduce.ts +2 -7
- package/src/components/editor/renderers/vertical-layout/__tests__/vertical-layout.test.ts +64 -2
- package/src/components/editor/renderers/vertical-layout/utils.ts +73 -0
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +19 -49
- package/src/components/static-html/__tests__/static-banner.test.tsx +112 -0
- package/src/components/static-html/static-banner.tsx +121 -68
- package/src/core/cells/__tests__/collapseConsoleOutputs.test.ts +35 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { O as e, nt as t, tt as n } from "./html-to-image-
|
|
1
|
+
import { O as e, nt as t, tt as n } from "./html-to-image-BMTAw0YV.js";
|
|
2
2
|
import { u as r } from "./dist-wdOJ5T7-.js";
|
|
3
3
|
import { b as i } from "./select-DQ9jU_B_.js";
|
|
4
4
|
import { n as a } from "./semaphore-89jsoHtS.js";
|
|
@@ -2,9 +2,9 @@ import { a as e } from "./rolldown-runtime-Cb76FRpK.js";
|
|
|
2
2
|
import { c as t, g as n, h as r, m as i, t as a } from "./button-DlqUC8W-.js";
|
|
3
3
|
import { t as o } from "./react-DMWcHin-.js";
|
|
4
4
|
import { t as s } from "./compiler-runtime-CAH9RLw2.js";
|
|
5
|
-
import { Ot as c, ot as l } from "./html-to-image-
|
|
5
|
+
import { Ot as c, ot as l } from "./html-to-image-BMTAw0YV.js";
|
|
6
6
|
import { u } from "./dist-wdOJ5T7-.js";
|
|
7
|
-
import { at as d, c as f, g as p, h as m, hn as h, l as g, mn as _, n as v, o as y, ot as b, r as x, st as S, t as C, u as w, vn as T } from "./common-
|
|
7
|
+
import { at as d, c as f, g as p, h as m, hn as h, l as g, mn as _, n as v, o as y, ot as b, r as x, st as S, t as C, u as w, vn as T } from "./common-FuL_5-mY.js";
|
|
8
8
|
import { R as E } from "./select-DQ9jU_B_.js";
|
|
9
9
|
import { Y as D } from "./dropdown-menu-Baf29jmS.js";
|
|
10
10
|
import { t as O } from "./jsx-runtime-CpoKNRgu.js";
|
|
@@ -672,4 +672,4 @@ function Z(e, t) {
|
|
|
672
672
|
return X(t || e, e);
|
|
673
673
|
}
|
|
674
674
|
//#endregion
|
|
675
|
-
export {
|
|
675
|
+
export { u as _, G as a, V as c, M as d, C as f, d as g, v as h, K as i, B as l, b as m, X as n, q as o, S as p, W as r, U as s, Z as t, N as u, l as v };
|
|
@@ -2,7 +2,7 @@ import { a as e } from "./rolldown-runtime-Cb76FRpK.js";
|
|
|
2
2
|
import { g as t, h as n, m as r } from "./button-DlqUC8W-.js";
|
|
3
3
|
import { t as i } from "./react-DMWcHin-.js";
|
|
4
4
|
import { t as a } from "./compiler-runtime-CAH9RLw2.js";
|
|
5
|
-
import {
|
|
5
|
+
import { c as o, v as s } from "./toDate-DfFzRDkN.js";
|
|
6
6
|
import { t as c } from "./jsx-runtime-CpoKNRgu.js";
|
|
7
7
|
import { n as l } from "./error-banner-xVkWHg51.js";
|
|
8
8
|
import { t as u } from "./tooltip-7p_32zdw.js";
|
|
@@ -11,7 +11,7 @@ import { j as f, n as p } from "./useTheme-C2ZreN00.js";
|
|
|
11
11
|
import { c as m } from "./arrays-CN1djC4k.js";
|
|
12
12
|
import { t as ee } from "./objects-CzUgoMIF.js";
|
|
13
13
|
import { t as h } from "./useAsyncData-Cyh2jLTR.js";
|
|
14
|
-
import { a as te, i as ne, n as re, t as g } from "./formats-
|
|
14
|
+
import { a as te, i as ne, n as re, t as g } from "./formats-nPPeLOk2.js";
|
|
15
15
|
import { A as ie, n as ae } from "./vega-loader.browser-DEggZzni.js";
|
|
16
16
|
import { t as oe } from "./react-vega-DSD5aT7_.js";
|
|
17
17
|
import { a as se, n as _ } from "./loader-D8jS1unC.js";
|
|
@@ -21,7 +21,7 @@ import { t as ue } from "./use-vega-container-remeasure-DUW4tRxq.js";
|
|
|
21
21
|
//#region src/plugins/impl/vega/fix-relative-url.ts
|
|
22
22
|
var v = /* @__PURE__ */ e(i(), 1), de = a();
|
|
23
23
|
function fe(e) {
|
|
24
|
-
return e.data && "url" in e.data && (e.data.url =
|
|
24
|
+
return e.data && "url" in e.data && (e.data.url = o(e.data.url).href), e;
|
|
25
25
|
}
|
|
26
26
|
//#endregion
|
|
27
27
|
//#region src/plugins/impl/vega/types.ts
|
|
@@ -486,12 +486,12 @@ async function U(e) {
|
|
|
486
486
|
}), !e.data || !("url" in e.data)) return e;
|
|
487
487
|
let i;
|
|
488
488
|
try {
|
|
489
|
-
i =
|
|
489
|
+
i = o(e.data.url);
|
|
490
490
|
} catch {
|
|
491
491
|
return e;
|
|
492
492
|
}
|
|
493
|
-
let a = e.data.format,
|
|
494
|
-
return t[i.pathname] =
|
|
493
|
+
let a = e.data.format, s = await n.run(() => _(i.href, a));
|
|
494
|
+
return t[i.pathname] = s, {
|
|
495
495
|
...e,
|
|
496
496
|
data: { name: i.pathname }
|
|
497
497
|
};
|
|
@@ -524,7 +524,7 @@ var G = (e) => {
|
|
|
524
524
|
embedOptions: s
|
|
525
525
|
}), t[5] = i, t[6] = s, t[7] = a, t[8] = d, t[9] = r, t[10] = n, t[11] = p) : p = t[11], p;
|
|
526
526
|
}, K = (e) => {
|
|
527
|
-
let i = (0, de.c)(50), { value: a, setValue:
|
|
527
|
+
let i = (0, de.c)(50), { value: a, setValue: o, chartSelection: c, fieldSelection: l, spec: m, embedOptions: h } = e, { theme: te } = p(), g = (0, v.useRef)(null), ae = (0, v.useRef)(void 0), [_, le] = (0, v.useState)(), y;
|
|
528
528
|
bb0: {
|
|
529
529
|
if (h && "actions" in h) {
|
|
530
530
|
y = h.actions;
|
|
@@ -544,12 +544,12 @@ var G = (e) => {
|
|
|
544
544
|
let w = C, E;
|
|
545
545
|
i[5] === w ? E = i[6] : (E = T(w), i[5] = w, i[6] = E);
|
|
546
546
|
let D = E, O;
|
|
547
|
-
i[7] !==
|
|
548
|
-
|
|
547
|
+
i[7] !== o || i[8] !== a ? (O = (e) => {
|
|
548
|
+
o({
|
|
549
549
|
...a,
|
|
550
550
|
...e
|
|
551
551
|
});
|
|
552
|
-
}, i[7] =
|
|
552
|
+
}, i[7] = o, i[8] = a, i[9] = O) : O = i[9];
|
|
553
553
|
let k = f(O), A;
|
|
554
554
|
i[10] === k ? A = i[11] : (A = d((e, n) => {
|
|
555
555
|
t.debug("[Vega signal]", e, n);
|
|
@@ -582,7 +582,7 @@ var G = (e) => {
|
|
|
582
582
|
className: "text-xs flex flex-col",
|
|
583
583
|
children: e.map(ge)
|
|
584
584
|
}),
|
|
585
|
-
children: /* @__PURE__ */ (0, W.jsx)(
|
|
585
|
+
children: /* @__PURE__ */ (0, W.jsx)(s, { className: "absolute bottom-1 right-0 m-2 h-4 w-4 cursor-help text-muted-foreground hover:text-foreground" })
|
|
586
586
|
});
|
|
587
587
|
}, i[20] = D, i[21] = z);
|
|
588
588
|
let B = z, V = te === "dark" ? "dark" : void 0, H;
|
package/package.json
CHANGED
|
@@ -101,7 +101,10 @@ describe("TerminalBuffer", () => {
|
|
|
101
101
|
buffer.writeChar("b");
|
|
102
102
|
buffer.handleEscape("\u001B[1A"); // Move up 1
|
|
103
103
|
buffer.writeChar("X");
|
|
104
|
-
expect(buffer.render()).toMatchInlineSnapshot(`
|
|
104
|
+
expect(buffer.render()).toMatchInlineSnapshot(`
|
|
105
|
+
"aX
|
|
106
|
+
b"
|
|
107
|
+
`);
|
|
105
108
|
});
|
|
106
109
|
|
|
107
110
|
test("handleEscape cursor down", () => {
|
|
@@ -328,6 +331,24 @@ describe("AnsiReducer", () => {
|
|
|
328
331
|
expect(result).toMatchInlineSnapshot(`"axy"`);
|
|
329
332
|
});
|
|
330
333
|
|
|
334
|
+
test("cursor up preserves lines below the cursor", () => {
|
|
335
|
+
const reducer = new AnsiReducer();
|
|
336
|
+
reducer.append("outer: 0%");
|
|
337
|
+
reducer.append("\n");
|
|
338
|
+
reducer.append("inner: 0%");
|
|
339
|
+
reducer.append("\u001B[A");
|
|
340
|
+
|
|
341
|
+
expect(reducer.render()).toBe("outer: 0%\ninner: 0%");
|
|
342
|
+
|
|
343
|
+
// tqdm writes nested progress updates as separate, flushed chunks. The
|
|
344
|
+
// newline moves back to the preserved inner line before it is rewritten.
|
|
345
|
+
reducer.append("\n");
|
|
346
|
+
reducer.append("\rinner: 1%");
|
|
347
|
+
reducer.append("\u001B[A");
|
|
348
|
+
|
|
349
|
+
expect(reducer.render()).toBe("outer: 0%\ninner: 1%");
|
|
350
|
+
});
|
|
351
|
+
|
|
331
352
|
test("reduce ignores control characters below space", () => {
|
|
332
353
|
const reducer = new AnsiReducer();
|
|
333
354
|
const result = reducer.reduce("hello\u0000\u0001\u0007world");
|
|
@@ -361,7 +382,11 @@ describe("ansiReduce", () => {
|
|
|
361
382
|
|
|
362
383
|
test("multi-line with cursor movement", () => {
|
|
363
384
|
const result = ansiReduce("Line 1\nLine 2\nLine 3\u001B[2AModified");
|
|
364
|
-
expect(result).toMatchInlineSnapshot(`
|
|
385
|
+
expect(result).toMatchInlineSnapshot(`
|
|
386
|
+
"Line 1Modified
|
|
387
|
+
Line 2
|
|
388
|
+
Line 3"
|
|
389
|
+
`);
|
|
365
390
|
});
|
|
366
391
|
|
|
367
392
|
test("erase and rewrite", () => {
|
|
@@ -434,7 +459,8 @@ describe("ansiReduce", () => {
|
|
|
434
459
|
Running chain 0: 100%|█████████████████████████████| 101000/101000 [00:07<00:00, 12749.04it/s]
|
|
435
460
|
Running chain 1: 100%|█████████████████████████████| 101000/101000 [00:08<00:00, 12442.67it/s]
|
|
436
461
|
Running chain 2: 100%|█████████████████████████████| 101000/101000 [00:08<00:00, 12373.90it/s]
|
|
437
|
-
|
|
462
|
+
Running chain 2: 95%|████████████████████████████▌ | 95950/101000 [00:07<00:00, 12922.54it/s]
|
|
463
|
+
Running chain 2: 100%|█████████████████████████████| 101000/101000 [00:08<00:00, 14869.01it/s]"
|
|
438
464
|
`);
|
|
439
465
|
});
|
|
440
466
|
});
|
|
@@ -475,7 +501,8 @@ describe("AnsiReducer streaming with append()", () => {
|
|
|
475
501
|
reducer.append("XYZ");
|
|
476
502
|
expect(reducer.render()).toMatchInlineSnapshot(`
|
|
477
503
|
"abc
|
|
478
|
-
XYZ
|
|
504
|
+
XYZ
|
|
505
|
+
"
|
|
479
506
|
`);
|
|
480
507
|
});
|
|
481
508
|
|
|
@@ -552,14 +579,12 @@ describe("AnsiReducer color preservation", () => {
|
|
|
552
579
|
test("preserves color codes with cursor movements", () => {
|
|
553
580
|
const reducer = new AnsiReducer();
|
|
554
581
|
// Test that color codes work alongside cursor movements
|
|
555
|
-
// Note: when cursor moves up, lines below are discarded (tqdm behavior)
|
|
556
582
|
const result = reducer.reduce(
|
|
557
583
|
"Line1\n\u001B[31mRed\u001B[0m\u001B[1A\u001B[32mGreen\u001B[0m",
|
|
558
584
|
);
|
|
559
|
-
//
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
`"Line1 \u001B[32mGreen\u001B[0m"`,
|
|
585
|
+
// Green is written at the end of row 0; row 1 remains intact.
|
|
586
|
+
expect(result).toBe(
|
|
587
|
+
"Line1 \u001B[32mGreen\u001B[0m\n\u001B[31mRed\u001B[0m",
|
|
563
588
|
);
|
|
564
589
|
});
|
|
565
590
|
});
|
|
@@ -623,7 +648,8 @@ describe("StatefulOutputMessage", () => {
|
|
|
623
648
|
|
|
624
649
|
expect(stateful.data).toMatchInlineSnapshot(`
|
|
625
650
|
"Line 1
|
|
626
|
-
Xine 2
|
|
651
|
+
Xine 2
|
|
652
|
+
"
|
|
627
653
|
`);
|
|
628
654
|
});
|
|
629
655
|
});
|
|
@@ -25,16 +25,11 @@ export class TerminalBuffer {
|
|
|
25
25
|
|
|
26
26
|
/** Move cursor by relative offsets. */
|
|
27
27
|
private moveCursor(rowDelta: number, colDelta: number) {
|
|
28
|
-
const oldRow = this.cursor.row;
|
|
29
28
|
this.cursor.row = Math.max(0, this.cursor.row + rowDelta);
|
|
30
29
|
this.cursor.col = Math.max(0, this.cursor.col + colDelta);
|
|
31
30
|
this.ensureLine(this.cursor.row);
|
|
32
|
-
|
|
33
|
-
//
|
|
34
|
-
// This simulates a fixed terminal window where tqdm overwrites content
|
|
35
|
-
if (rowDelta < 0 && this.cursor.row < oldRow) {
|
|
36
|
-
this.lines.splice(this.cursor.row + 1);
|
|
37
|
-
}
|
|
31
|
+
// Cursor movement does not erase content. This matters for multi-line
|
|
32
|
+
// progress renderers, which move up before rewriting a preserved row.
|
|
38
33
|
}
|
|
39
34
|
|
|
40
35
|
/** Move cursor to a specific absolute position. */
|
|
@@ -1,8 +1,70 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
/* oxlint-disable typescript/no-explicit-any */
|
|
3
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
4
4
|
import type { OutputMessage } from "@/core/kernel/messages";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
getInitialShowCode,
|
|
7
|
+
groupCellsByColumn,
|
|
8
|
+
shouldHideCode,
|
|
9
|
+
updateShowCodeQueryParam,
|
|
10
|
+
} from "../utils";
|
|
11
|
+
|
|
12
|
+
const originalHref = window.location.href;
|
|
13
|
+
let originalStaticState: typeof window.__MARIMO_STATIC__;
|
|
14
|
+
|
|
15
|
+
describe("show code", () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
originalStaticState = window.__MARIMO_STATIC__;
|
|
18
|
+
window.history.replaceState(null, "", "/?existing=value#section");
|
|
19
|
+
window.__MARIMO_STATIC__ = {
|
|
20
|
+
files: {},
|
|
21
|
+
modelNotifications: [],
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
window.history.replaceState(null, "", originalHref);
|
|
27
|
+
window.__MARIMO_STATIC__ = originalStaticState;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("does not add a query parameter when reading the initial value", () => {
|
|
31
|
+
expect(
|
|
32
|
+
getInitialShowCode({
|
|
33
|
+
showCodeInRunModePreference: true,
|
|
34
|
+
kioskMode: false,
|
|
35
|
+
}),
|
|
36
|
+
).toBe(true);
|
|
37
|
+
expect(window.location.search).toBe("?existing=value");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("reflects static notebook toggles in the URL", () => {
|
|
41
|
+
updateShowCodeQueryParam(false);
|
|
42
|
+
expect(window.location.search).toBe("?existing=value&show-code=false");
|
|
43
|
+
expect(window.location.hash).toBe("#section");
|
|
44
|
+
expect(
|
|
45
|
+
getInitialShowCode({
|
|
46
|
+
showCodeInRunModePreference: true,
|
|
47
|
+
kioskMode: false,
|
|
48
|
+
}),
|
|
49
|
+
).toBe(false);
|
|
50
|
+
|
|
51
|
+
updateShowCodeQueryParam(true);
|
|
52
|
+
expect(window.location.search).toBe("?existing=value&show-code=true");
|
|
53
|
+
expect(
|
|
54
|
+
getInitialShowCode({
|
|
55
|
+
showCodeInRunModePreference: true,
|
|
56
|
+
kioskMode: false,
|
|
57
|
+
}),
|
|
58
|
+
).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("does not reflect non-static notebook toggles in the URL", () => {
|
|
62
|
+
delete window.__MARIMO_STATIC__;
|
|
63
|
+
|
|
64
|
+
updateShowCodeQueryParam(false);
|
|
65
|
+
expect(window.location.search).toBe("?existing=value");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
6
68
|
|
|
7
69
|
describe("groupCellsByColumn", () => {
|
|
8
70
|
it("should group cells by column and maintain order", () => {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { isOutputEmpty } from "@/core/cells/outputs";
|
|
4
|
+
import type { CellData, CellRuntimeState } from "@/core/cells/types";
|
|
5
|
+
import { MarkdownLanguageAdapter } from "@/core/codemirror/language/languages/markdown";
|
|
6
|
+
import { KnownQueryParams } from "@/core/constants";
|
|
7
|
+
import type { OutputMessage } from "@/core/kernel/messages";
|
|
8
|
+
import { isStaticNotebook } from "@/core/static/static-state";
|
|
9
|
+
import { isWasm } from "@/core/wasm/utils";
|
|
10
|
+
import { updateQueryParams } from "@/utils/urls";
|
|
11
|
+
|
|
12
|
+
export function getInitialShowCode({
|
|
13
|
+
showCodeInRunModePreference,
|
|
14
|
+
kioskMode,
|
|
15
|
+
}: {
|
|
16
|
+
showCodeInRunModePreference: boolean;
|
|
17
|
+
kioskMode: boolean;
|
|
18
|
+
}): boolean {
|
|
19
|
+
// The mount option takes precedence over other defaults.
|
|
20
|
+
if (!showCodeInRunModePreference) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const showCodeByQueryParam = new URLSearchParams(window.location.search).get(
|
|
25
|
+
KnownQueryParams.showCode,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// Static notebooks, WASM notebooks, and kiosk mode show code by default.
|
|
29
|
+
return showCodeByQueryParam === null
|
|
30
|
+
? isStaticNotebook() || isWasm() || kioskMode
|
|
31
|
+
: showCodeByQueryParam === "true";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function updateShowCodeQueryParam(showCode: boolean): void {
|
|
35
|
+
// Persist explicit user choices in shareable static notebook URLs. Avoid
|
|
36
|
+
// adding the parameter on startup so unchanged URLs retain their defaults.
|
|
37
|
+
if (isStaticNotebook()) {
|
|
38
|
+
updateQueryParams((params) => {
|
|
39
|
+
params.set(KnownQueryParams.showCode, String(showCode));
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function groupCellsByColumn(
|
|
45
|
+
cells: (CellRuntimeState & CellData)[],
|
|
46
|
+
): [number, (CellRuntimeState & CellData)[]][] {
|
|
47
|
+
// Group cells by column
|
|
48
|
+
const cellsByColumn = new Map<number, (CellRuntimeState & CellData)[]>();
|
|
49
|
+
let lastSeenColumn = 0;
|
|
50
|
+
cells.forEach((cell) => {
|
|
51
|
+
const column = cell.config.column ?? lastSeenColumn;
|
|
52
|
+
lastSeenColumn = column;
|
|
53
|
+
if (!cellsByColumn.has(column)) {
|
|
54
|
+
cellsByColumn.set(column, []);
|
|
55
|
+
}
|
|
56
|
+
cellsByColumn.get(column)?.push(cell);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Sort columns by index
|
|
60
|
+
return [...cellsByColumn.entries()].toSorted(([a], [b]) => a - b);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Determine if the code should be hidden.
|
|
65
|
+
*
|
|
66
|
+
* This is used to hide the code if it's pure markdown and there's an output,
|
|
67
|
+
* or if the code is empty.
|
|
68
|
+
*/
|
|
69
|
+
export function shouldHideCode(code: string, output: OutputMessage | null) {
|
|
70
|
+
const isPureMarkdown = new MarkdownLanguageAdapter().isSupported(code);
|
|
71
|
+
const hasOutput = output !== null && !isOutputEmpty(output);
|
|
72
|
+
return (isPureMarkdown && hasOutput) || code.trim() === "";
|
|
73
|
+
}
|
|
@@ -27,13 +27,11 @@ import {
|
|
|
27
27
|
} from "@/components/ui/dropdown-menu";
|
|
28
28
|
import { outputIsLoading, outputIsStale } from "@/core/cells/cell";
|
|
29
29
|
import type { CellId } from "@/core/cells/ids";
|
|
30
|
-
import { isOutputEmpty } from "@/core/cells/outputs";
|
|
31
30
|
import type { CellData, CellRuntimeState } from "@/core/cells/types";
|
|
32
31
|
import { getReadonlyCodeDisplay } from "@/core/cells/readonly-code-display";
|
|
33
32
|
import { MarkdownLanguageAdapter } from "@/core/codemirror/language/languages/markdown";
|
|
34
33
|
import { useResolvedMarimoConfig } from "@/core/config/config";
|
|
35
|
-
import { CSSClasses
|
|
36
|
-
import type { OutputMessage } from "@/core/kernel/messages";
|
|
34
|
+
import { CSSClasses } from "@/core/constants";
|
|
37
35
|
import { kernelStateAtom } from "@/core/kernel/state";
|
|
38
36
|
import { useNotebookCodeAvailable } from "@/core/meta/code-visibility";
|
|
39
37
|
import { showCodeInRunModeAtom } from "@/core/meta/state";
|
|
@@ -46,7 +44,6 @@ import { useRequestClient } from "@/core/network/requests";
|
|
|
46
44
|
import type { CellConfig } from "@/core/network/types";
|
|
47
45
|
import { downloadAsHTML } from "@/core/static/download-html";
|
|
48
46
|
import { isStaticNotebook } from "@/core/static/static-state";
|
|
49
|
-
import { isWasm } from "@/core/wasm/utils";
|
|
50
47
|
import { cn } from "@/utils/cn";
|
|
51
48
|
import {
|
|
52
49
|
ADD_PRINTING_CLASS,
|
|
@@ -58,6 +55,12 @@ import { FloatingOutline } from "../../chrome/panels/outline/floating-outline";
|
|
|
58
55
|
import { cellDomProps } from "../../common";
|
|
59
56
|
import type { ICellRendererPlugin, ICellRendererProps } from "../types";
|
|
60
57
|
import { useDelayVisibility } from "./useDelayVisibility";
|
|
58
|
+
import {
|
|
59
|
+
getInitialShowCode,
|
|
60
|
+
groupCellsByColumn,
|
|
61
|
+
shouldHideCode,
|
|
62
|
+
updateShowCodeQueryParam,
|
|
63
|
+
} from "./utils";
|
|
61
64
|
import { VerticalLayoutWrapper } from "./vertical-layout-wrapper";
|
|
62
65
|
|
|
63
66
|
type VerticalLayout = null;
|
|
@@ -74,22 +77,20 @@ const VerticalLayoutRenderer: React.FC<VerticalLayoutProps> = ({
|
|
|
74
77
|
const [userConfig] = useResolvedMarimoConfig();
|
|
75
78
|
const showCodeInRunModePreference = useAtomValue(showCodeInRunModeAtom);
|
|
76
79
|
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (!showCodeInRunModePreference) {
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
// If 'auto' or not found, use URL param
|
|
84
|
-
// If url param is not set, we default to true for static notebooks, wasm notebooks, and kiosk mode
|
|
85
|
-
const showCodeByQueryParam = urlParams.get(KnownQueryParams.showCode);
|
|
86
|
-
return showCodeByQueryParam === null
|
|
87
|
-
? isStaticNotebook() || isWasm() || kioskMode
|
|
88
|
-
: showCodeByQueryParam === "true";
|
|
89
|
-
});
|
|
80
|
+
const [showCode, setShowCode] = useState(() =>
|
|
81
|
+
getInitialShowCode({ showCodeInRunModePreference, kioskMode }),
|
|
82
|
+
);
|
|
90
83
|
|
|
91
84
|
const canShowCode = useNotebookCodeAvailable(cells);
|
|
92
85
|
|
|
86
|
+
const handleToggleShowCode = () => {
|
|
87
|
+
setShowCode((currentShowCode) => {
|
|
88
|
+
const nextShowCode = !currentShowCode;
|
|
89
|
+
updateShowCodeQueryParam(nextShowCode);
|
|
90
|
+
return nextShowCode;
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
|
|
93
94
|
const renderCell = (cell: CellRuntimeState & CellData) => {
|
|
94
95
|
return (
|
|
95
96
|
<VerticalCell
|
|
@@ -169,7 +170,7 @@ const VerticalLayoutRenderer: React.FC<VerticalLayoutProps> = ({
|
|
|
169
170
|
<ActionButtons
|
|
170
171
|
canShowCode={canShowCode}
|
|
171
172
|
showCode={showCode}
|
|
172
|
-
onToggleShowCode={
|
|
173
|
+
onToggleShowCode={handleToggleShowCode}
|
|
173
174
|
/>
|
|
174
175
|
)}
|
|
175
176
|
<FloatingOutline />
|
|
@@ -469,34 +470,3 @@ export const VerticalLayoutPlugin: ICellRendererPlugin<
|
|
|
469
470
|
serializeLayout: (layout) => layout,
|
|
470
471
|
getInitialLayout: () => null,
|
|
471
472
|
};
|
|
472
|
-
|
|
473
|
-
export function groupCellsByColumn(
|
|
474
|
-
cells: (CellRuntimeState & CellData)[],
|
|
475
|
-
): [number, (CellRuntimeState & CellData)[]][] {
|
|
476
|
-
// Group cells by column
|
|
477
|
-
const cellsByColumn = new Map<number, (CellRuntimeState & CellData)[]>();
|
|
478
|
-
let lastSeenColumn = 0;
|
|
479
|
-
cells.forEach((cell) => {
|
|
480
|
-
const column = cell.config.column ?? lastSeenColumn;
|
|
481
|
-
lastSeenColumn = column;
|
|
482
|
-
if (!cellsByColumn.has(column)) {
|
|
483
|
-
cellsByColumn.set(column, []);
|
|
484
|
-
}
|
|
485
|
-
cellsByColumn.get(column)?.push(cell);
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
// Sort columns by index
|
|
489
|
-
return [...cellsByColumn.entries()].toSorted(([a], [b]) => a - b);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
/**
|
|
493
|
-
* Determine if the code should be hidden.
|
|
494
|
-
*
|
|
495
|
-
* This is used to hide the code if it's pure markdown and there's an output,
|
|
496
|
-
* or if the code is empty.
|
|
497
|
-
*/
|
|
498
|
-
export function shouldHideCode(code: string, output: OutputMessage | null) {
|
|
499
|
-
const isPureMarkdown = new MarkdownLanguageAdapter().isSupported(code);
|
|
500
|
-
const hasOutput = output !== null && !isOutputEmpty(output);
|
|
501
|
-
return (isPureMarkdown && hasOutput) || code.trim() === "";
|
|
502
|
-
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
4
|
+
import { createStore, Provider } from "jotai";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { userConfigAtom } from "@/core/config/config";
|
|
7
|
+
import { defaultUserConfig } from "@/core/config/config-schema";
|
|
8
|
+
import { codeAtom, filenameAtom } from "@/core/saving/file-state";
|
|
9
|
+
import { getStaticNotebookRunTarget, StaticBanner } from "../static-banner";
|
|
10
|
+
|
|
11
|
+
function renderBanner(sharing?: {
|
|
12
|
+
html?: boolean;
|
|
13
|
+
wasm?: boolean;
|
|
14
|
+
molab?: boolean;
|
|
15
|
+
}) {
|
|
16
|
+
const store = createStore();
|
|
17
|
+
store.set(codeAtom, "import marimo\n");
|
|
18
|
+
store.set(filenameAtom, "/notebooks/my notebook.py");
|
|
19
|
+
store.set(userConfigAtom, {
|
|
20
|
+
...defaultUserConfig(),
|
|
21
|
+
sharing,
|
|
22
|
+
});
|
|
23
|
+
window.__MARIMO_STATIC__ = { files: {} };
|
|
24
|
+
|
|
25
|
+
return render(
|
|
26
|
+
<Provider store={store}>
|
|
27
|
+
<StaticBanner />
|
|
28
|
+
</Provider>,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function openDialog() {
|
|
33
|
+
fireEvent.click(screen.getByTestId("static-notebook-dialog-trigger"));
|
|
34
|
+
return screen.findByRole("dialog");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("StaticBanner", () => {
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
delete window.__MARIMO_STATIC__;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("shows the molab option by default", async () => {
|
|
43
|
+
renderBanner();
|
|
44
|
+
await openDialog();
|
|
45
|
+
|
|
46
|
+
expect(screen.getByRole("link", { name: "Open in molab" })).toHaveAttribute(
|
|
47
|
+
"href",
|
|
48
|
+
expect.stringContaining("molab.marimo.io/new"),
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("hides the molab option when sharing is disabled", async () => {
|
|
53
|
+
renderBanner({ html: false, wasm: false, molab: false });
|
|
54
|
+
await openDialog();
|
|
55
|
+
|
|
56
|
+
expect(
|
|
57
|
+
screen.queryByRole("link", { name: "Open in molab" }),
|
|
58
|
+
).not.toBeInTheDocument();
|
|
59
|
+
expect(
|
|
60
|
+
screen.queryByText(/cloud-hosted notebook platform/),
|
|
61
|
+
).not.toBeInTheDocument();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("offers uv, pip, and conda commands", async () => {
|
|
65
|
+
renderBanner({ molab: false });
|
|
66
|
+
await openDialog();
|
|
67
|
+
|
|
68
|
+
expect(screen.getByRole("tab", { name: "uv" })).toBeVisible();
|
|
69
|
+
expect(
|
|
70
|
+
screen.getByText("uvx marimo edit --sandbox 'my notebook.py'"),
|
|
71
|
+
).toBeVisible();
|
|
72
|
+
|
|
73
|
+
fireEvent.mouseDown(screen.getByRole("tab", { name: "pip" }), {
|
|
74
|
+
button: 0,
|
|
75
|
+
});
|
|
76
|
+
expect(screen.getByText(/pip install marimo/)).toBeVisible();
|
|
77
|
+
|
|
78
|
+
fireEvent.mouseDown(screen.getByRole("tab", { name: "conda" }), {
|
|
79
|
+
button: 0,
|
|
80
|
+
});
|
|
81
|
+
expect(
|
|
82
|
+
screen.getByText(/conda install -c conda-forge marimo/),
|
|
83
|
+
).toBeVisible();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("getStaticNotebookRunTarget", () => {
|
|
88
|
+
it.each([
|
|
89
|
+
["file:///tmp/notebook.html", "notebook.py"],
|
|
90
|
+
["http://localhost:8000/report", "notebook.py"],
|
|
91
|
+
["https://example.com/report", "notebook.py"],
|
|
92
|
+
])("falls back to the downloaded filename for %s", (href, expected) => {
|
|
93
|
+
expect(getStaticNotebookRunTarget(href, "notebook.py")).toBe(expected);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it.each([
|
|
97
|
+
[
|
|
98
|
+
"https://example.com/notebook.html#section",
|
|
99
|
+
"https://example.com/notebook.html",
|
|
100
|
+
],
|
|
101
|
+
[
|
|
102
|
+
"https://static.marimo.app/static/example",
|
|
103
|
+
"https://static.marimo.app/static/example",
|
|
104
|
+
],
|
|
105
|
+
[
|
|
106
|
+
"https://molab.marimo.io/notebooks/nb_example/app",
|
|
107
|
+
"https://molab.marimo.io/notebooks/nb_example/app",
|
|
108
|
+
],
|
|
109
|
+
])("uses URLs supported by marimo edit: %s", (href, expected) => {
|
|
110
|
+
expect(getStaticNotebookRunTarget(href, "notebook.py")).toBe(expected);
|
|
111
|
+
});
|
|
112
|
+
});
|