@marimo-team/islands 0.24.1-dev113 → 0.24.1-dev115

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 (65) hide show
  1. package/dist/{ConnectedDataExplorerComponent-CF7VGRML.js → ConnectedDataExplorerComponent-3vyrwCHu.js} +12 -12
  2. package/dist/{ErrorBoundary-Ca9Ulp6E.js → ErrorBoundary-QF0xsxMw.js} +3 -3
  3. package/dist/{any-language-editor-D6lJfTIq.js → any-language-editor-B2e3EkTe.js} +4 -4
  4. package/dist/{button-BeWeF5Z2.js → button-CKBkqR9J.js} +1 -1
  5. package/dist/{capabilities-C7Q0J4_N.js → capabilities-CMBBJTcc.js} +1 -1
  6. package/dist/{chat-ui-DwtGBnE7.js → chat-ui-BgGHMO2G.js} +16 -16
  7. package/dist/{check-DGKNz9MB.js → check-C_-c2CXl.js} +1 -1
  8. package/dist/{common-CHiGez3b.js → common-Cdx64lyR.js} +23 -23
  9. package/dist/{copy-T56I_gVc.js → copy-D4GHy7GA.js} +2 -2
  10. package/dist/{dist-CSjGX93W.js → dist-mymH-A_x.js} +2 -2
  11. package/dist/{dropdown-menu-D-pePvPo.js → dropdown-menu-Div6gwlw.js} +6 -6
  12. package/dist/{error-banner-C0gx40vn.js → error-banner-DmgOcFS3.js} +2 -2
  13. package/dist/{esm-aqYR5Dl6.js → esm-CmFEQJjP.js} +1 -1
  14. package/dist/{extends-G2pVMdwU.js → extends-CGTKDWHv.js} +4 -4
  15. package/dist/{formats-CX_fkiT0.js → formats-DuRRWVwb.js} +3 -3
  16. package/dist/{glide-data-editor-BXnH2j9C.js → glide-data-editor-ekJ5GwaZ.js} +8 -8
  17. package/dist/{glide-utils-AvKWWLSq.js → glide-utils-Dwc8Eh8G.js} +3 -3
  18. package/dist/{html-to-image-Cf3vfRXj.js → html-to-image-u8Ik6EnK.js} +20 -20
  19. package/dist/{label-BUGMEGks.js → label-DdGS_CZZ.js} +2 -2
  20. package/dist/{loader-BIZus5lj.js → loader-VZ__bv4g.js} +1 -1
  21. package/dist/main.js +38 -38
  22. package/dist/{mermaid-CzrpWuKV.js → mermaid-Bt45Zzl4.js} +4 -4
  23. package/dist/{process-output-BZs2qaau.js → process-output-DQiqt-_U.js} +3 -3
  24. package/dist/{reveal-component-BSz_VbT2.js → reveal-component-tQ-rgxcQ.js} +9 -9
  25. package/dist/{schemas-CLB4UWgG.js → schemas-BjeOpZLk.js} +1 -1
  26. package/dist/{select-foAi7oM_.js → select-DotAQ4jp.js} +12 -12
  27. package/dist/{spec-JFqQpvZq.js → spec-BesaYkGC.js} +4 -4
  28. package/dist/style.css +1 -1
  29. package/dist/{swiper-component-Bqzl6az1.js → swiper-component-C90YjhaV.js} +2 -2
  30. package/dist/{toDate-DxgaJjev.js → toDate-CrAIOASL.js} +3 -3
  31. package/dist/{tooltip-B1X8ECYn.js → tooltip-COXZxyl7.js} +3 -3
  32. package/dist/{useAsyncData-BkvtR_Y2.js → useAsyncData-x0_tWDWI.js} +1 -1
  33. package/dist/{useDateFormatter-DGIhtjdF.js → useDateFormatter-B3V__WqX.js} +2 -2
  34. package/dist/{useDeepCompareMemoize-DcZEHOmt.js → useDeepCompareMemoize-KaRHJEEU.js} +1 -1
  35. package/dist/{useIframeCapabilities-Bj4ORYGz.js → useIframeCapabilities-De3C-9b4.js} +1 -1
  36. package/dist/{useLifecycle-BHpzFL9q.js → useLifecycle-x0OriHo-.js} +10 -10
  37. package/dist/{useTheme-BlOgV7zf.js → useTheme-Bm9_FyqO.js} +2 -2
  38. package/dist/{vega-component-B4rwjseP.js → vega-component-nD2oP6WN.js} +9 -9
  39. package/package.json +1 -1
  40. package/src/components/dependency-graph/__tests__/elements.test.ts +151 -0
  41. package/src/components/dependency-graph/custom-edge.tsx +68 -0
  42. package/src/components/dependency-graph/custom-node.tsx +54 -26
  43. package/src/components/dependency-graph/dependency-graph-tree.tsx +110 -21
  44. package/src/components/dependency-graph/elements.ts +225 -31
  45. package/src/components/dependency-graph/panels.tsx +23 -2
  46. package/src/components/dependency-graph/utils/__tests__/layout.test.ts +78 -0
  47. package/src/components/dependency-graph/utils/layout.ts +108 -18
  48. package/src/components/ui/__tests__/badge.test.ts +63 -0
  49. package/src/components/ui/__tests__/button.test.ts +19 -0
  50. package/src/components/ui/__tests__/menu-items.test.ts +31 -0
  51. package/src/components/ui/badge.tsx +9 -7
  52. package/src/components/ui/button.tsx +1 -0
  53. package/src/components/ui/combobox.tsx +1 -1
  54. package/src/components/ui/date-input.tsx +1 -1
  55. package/src/components/ui/dialog.tsx +3 -1
  56. package/src/components/ui/menu-items.tsx +9 -9
  57. package/src/components/ui/native-select.tsx +2 -2
  58. package/src/components/ui/navigation.tsx +1 -1
  59. package/src/components/ui/range-slider.tsx +5 -3
  60. package/src/components/ui/select.tsx +1 -1
  61. package/src/components/ui/sheet.tsx +1 -1
  62. package/src/components/ui/toast.tsx +8 -5
  63. package/src/components/ui/toaster.tsx +2 -2
  64. package/src/css/globals.css +28 -0
  65. package/src/plugins/impl/data-frames/forms/__tests__/__snapshots__/form.test.tsx.snap +25 -25
@@ -1,9 +1,9 @@
1
1
  import { a as e } from "./rolldown-runtime-Cb76FRpK.js";
2
- import { c as t, h as n, t as r } from "./button-BeWeF5Z2.js";
2
+ import { c as t, h as n, t as r } from "./button-CKBkqR9J.js";
3
3
  import { t as i } from "./react-DMWcHin-.js";
4
4
  import { t as a } from "./compiler-runtime-CAH9RLw2.js";
5
5
  import { t as o } from "./jsx-runtime-CpoKNRgu.js";
6
- import { t as s } from "./useIframeCapabilities-Bj4ORYGz.js";
6
+ import { t as s } from "./useIframeCapabilities-De3C-9b4.js";
7
7
  /* empty css */
8
8
  //#region ../node_modules/.pnpm/swiper@12.1.2/node_modules/swiper/shared/ssr-window.esm.mjs
9
9
  var c = /* @__PURE__ */ e(i(), 1), l = a();
@@ -1,8 +1,8 @@
1
1
  import { t as e } from "./rolldown-runtime-Cb76FRpK.js";
2
- import { g as t } from "./button-BeWeF5Z2.js";
2
+ import { g as t } from "./button-CKBkqR9J.js";
3
3
  import { t as n } from "./compiler-runtime-CAH9RLw2.js";
4
- import { u as r } from "./dist-CSjGX93W.js";
5
- import { E as i, _ as a, a as o, b as s } from "./useTheme-BlOgV7zf.js";
4
+ import { u as r } from "./dist-mymH-A_x.js";
5
+ import { E as i, _ as a, a as o, b as s } from "./useTheme-Bm9_FyqO.js";
6
6
  import { t as c } from "./invariant-DLmrl-hA.js";
7
7
  var l = r("circle-question-mark", [
8
8
  ["circle", {
@@ -1,10 +1,10 @@
1
1
  import { a as e } from "./rolldown-runtime-Cb76FRpK.js";
2
- import { d as t, h as n, p as r } from "./button-BeWeF5Z2.js";
2
+ import { d as t, h as n, p as r } from "./button-CKBkqR9J.js";
3
3
  import { t as i } from "./react-DMWcHin-.js";
4
4
  import { t as a } from "./compiler-runtime-CAH9RLw2.js";
5
- import { a as o, i as s, n as c, r as l, s as u, t as d } from "./dist-CSjGX93W.js";
5
+ import { a as o, i as s, n as c, r as l, s as u, t as d } from "./dist-mymH-A_x.js";
6
6
  import { t as f } from "./jsx-runtime-CpoKNRgu.js";
7
- import { Bc as p, Dc as m, Ec as h, Hc as ee, Kc as g, Lc as _, Pc as v, Vc as y, Wc as b, kc as x, qc as S } from "./schemas-CLB4UWgG.js";
7
+ import { Bc as p, Dc as m, Ec as h, Hc as ee, Kc as g, Lc as _, Pc as v, Vc as y, Wc as b, kc as x, qc as S } from "./schemas-BjeOpZLk.js";
8
8
  //#region ../node_modules/.pnpm/@radix-ui+react-tooltip@1.2.8_@types+react-dom@19.2.3_@types+react@19.2.10__@types+reac_6c936392a1aa34d496e116dc55299197/node_modules/@radix-ui/react-tooltip/dist/index.mjs
9
9
  var C = /* @__PURE__ */ e(i(), 1), w = f(), [T, E] = g("Tooltip", [o]), D = o(), O = "TooltipProvider", te = 700, k = "tooltip.open", [ne, A] = T(O), j = (e) => {
10
10
  let { __scopeTooltip: t, delayDuration: n = te, skipDelayDuration: r = 300, disableHoverableContent: i = !1, children: a } = e, o = C.useRef(!0), s = C.useRef(!1), c = C.useRef(0);
@@ -1,7 +1,7 @@
1
1
  import { a as e } from "./rolldown-runtime-Cb76FRpK.js";
2
2
  import { t } from "./react-DMWcHin-.js";
3
3
  import { t as n } from "./compiler-runtime-CAH9RLw2.js";
4
- import { j as r } from "./useTheme-BlOgV7zf.js";
4
+ import { j as r } from "./useTheme-Bm9_FyqO.js";
5
5
  import { t as i } from "./invariant-DLmrl-hA.js";
6
6
  //#region src/hooks/useAsyncData.ts
7
7
  var a = n(), o = /* @__PURE__ */ e(t(), 1), s = {
@@ -1,7 +1,7 @@
1
1
  import { a as e } from "./rolldown-runtime-Cb76FRpK.js";
2
2
  import { t } from "./react-DMWcHin-.js";
3
- import { u as n } from "./dist-CSjGX93W.js";
4
- import { E as r } from "./select-foAi7oM_.js";
3
+ import { u as n } from "./dist-mymH-A_x.js";
4
+ import { E as r } from "./select-DotAQ4jp.js";
5
5
  var i = n("chart-pie", [["path", {
6
6
  d: "M21 12c.552 0 1.005-.449.95-.998a10 10 0 0 0-8.953-8.951c-.55-.055-.998.398-.998.95v8a1 1 0 0 0 1 1z",
7
7
  key: "pzmjnu"
@@ -1,6 +1,6 @@
1
1
  import { a as e } from "./rolldown-runtime-Cb76FRpK.js";
2
2
  import { t } from "./react-DMWcHin-.js";
3
- import { A as n } from "./useTheme-BlOgV7zf.js";
3
+ import { A as n } from "./useTheme-Bm9_FyqO.js";
4
4
  //#region src/hooks/useDeepCompareMemoize.ts
5
5
  var r = /* @__PURE__ */ e(t(), 1);
6
6
  function i(e) {
@@ -1,5 +1,5 @@
1
1
  import { t as e } from "./compiler-runtime-CAH9RLw2.js";
2
- import { t } from "./capabilities-C7Q0J4_N.js";
2
+ import { t } from "./capabilities-CMBBJTcc.js";
3
3
  //#region src/hooks/useIframeCapabilities.ts
4
4
  var n = e();
5
5
  function r() {
@@ -1,10 +1,10 @@
1
1
  import { a as e } from "./rolldown-runtime-Cb76FRpK.js";
2
- import { g as t, h as n, r } from "./button-BeWeF5Z2.js";
2
+ import { g as t, h as n, r } from "./button-CKBkqR9J.js";
3
3
  import { t as i } from "./react-DMWcHin-.js";
4
4
  import { t as a } from "./compiler-runtime-CAH9RLw2.js";
5
- import { u as o } from "./dist-CSjGX93W.js";
5
+ import { u as o } from "./dist-mymH-A_x.js";
6
6
  import { t as s } from "./jsx-runtime-CpoKNRgu.js";
7
- import { E as c, w as l } from "./useTheme-BlOgV7zf.js";
7
+ import { E as c, w as l } from "./useTheme-Bm9_FyqO.js";
8
8
  var u = o("calendar", [
9
9
  ["path", {
10
10
  d: "M8 2v4",
@@ -136,14 +136,14 @@ function g(e, n, r) {
136
136
  }
137
137
  //#endregion
138
138
  //#region src/components/ui/badge.tsx
139
- var _ = /* @__PURE__ */ e(i(), 1), v = s(), y = r("inline-flex items-center border rounded-full px-2 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2", {
139
+ var _ = /* @__PURE__ */ e(i(), 1), v = s(), y = r("inline-flex items-center border rounded-full px-2 py-0.5 text-xs font-semibold transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 forced-color-adjust-none", {
140
140
  variants: { variant: {
141
- default: "bg-primary hover:bg-primary/60 border-transparent text-primary-foreground",
142
- defaultOutline: "bg-(--blue-2) border-(--blue-8) text-(--blue-11)",
143
- secondary: "bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground",
144
- destructive: "bg-(--red-2) border-(--red-6) text-(--red-9) hover:bg-(--red-3)",
145
- success: "bg-(--grass-2) border-(--grass-5) text-(--grass-9) hover:bg-(--grass-3)",
146
- outline: "text-foreground"
141
+ default: "bg-primary hover:bg-primary/60 border-transparent text-primary-foreground forced-colors:bg-[Highlight] forced-colors:border-[Highlight] forced-colors:text-[HighlightText] forced-colors:hover:bg-[Highlight] forced-colors:hover:text-[HighlightText]",
142
+ defaultOutline: "bg-(--blue-2) border-(--blue-8) text-(--blue-11) forced-colors:bg-[Canvas] forced-colors:border-[Highlight] forced-colors:text-[CanvasText]",
143
+ secondary: "bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground forced-colors:bg-[ButtonFace] forced-colors:border-[ButtonText] forced-colors:text-[ButtonText] forced-colors:hover:bg-[Highlight] forced-colors:hover:text-[HighlightText]",
144
+ destructive: "bg-(--red-2) border-(--red-6) text-(--red-9) hover:bg-(--red-3) forced-colors:bg-[Canvas] forced-colors:border-[ButtonText] forced-colors:text-[CanvasText] forced-colors:hover:bg-[Highlight] forced-colors:hover:text-[HighlightText]",
145
+ success: "bg-(--grass-2) border-(--grass-5) text-(--grass-9) hover:bg-(--grass-3) forced-colors:bg-[Canvas] forced-colors:border-[ButtonText] forced-colors:text-[CanvasText] forced-colors:hover:bg-[Highlight] forced-colors:hover:text-[HighlightText]",
146
+ outline: "text-foreground forced-colors:bg-[Canvas] forced-colors:border-[ButtonText] forced-colors:text-[CanvasText]"
147
147
  } },
148
148
  defaultVariants: { variant: "default" }
149
149
  }), b = (e) => {
@@ -1,8 +1,8 @@
1
1
  import { a as e } from "./rolldown-runtime-Cb76FRpK.js";
2
- import { a as t, g as n, s as r } from "./button-BeWeF5Z2.js";
2
+ import { a as t, g as n, s as r } from "./button-CKBkqR9J.js";
3
3
  import { t as i } from "./react-DMWcHin-.js";
4
4
  import { t as a } from "./compiler-runtime-CAH9RLw2.js";
5
- import { Cn as o, In as s, Jn as c, Sn as l, Tt as u, jn as d, kt as f, un as p, vt as m } from "./schemas-CLB4UWgG.js";
5
+ import { Cn as o, In as s, Jn as c, Sn as l, Tt as u, jn as d, kt as f, un as p, vt as m } from "./schemas-BjeOpZLk.js";
6
6
  import { n as h, t as g } from "./_baseMerge-Dv8bBloN.js";
7
7
  //#region ../node_modules/.pnpm/lodash-es@4.18.1/node_modules/lodash-es/mergeWith.js
8
8
  var _ = h(function(e, t, n, r) {
@@ -1,21 +1,21 @@
1
1
  import { a as e } from "./rolldown-runtime-Cb76FRpK.js";
2
- import { g as t, h as n, m as r, o as i } from "./button-BeWeF5Z2.js";
2
+ import { g as t, h as n, m as r, o as i } from "./button-CKBkqR9J.js";
3
3
  import { t as a } from "./react-DMWcHin-.js";
4
4
  import { t as o } from "./compiler-runtime-CAH9RLw2.js";
5
- import { c as s, v as c } from "./toDate-DxgaJjev.js";
5
+ import { c as s, v as c } from "./toDate-CrAIOASL.js";
6
6
  import { t as l } from "./jsx-runtime-CpoKNRgu.js";
7
- import { n as u } from "./error-banner-C0gx40vn.js";
8
- import { t as d } from "./tooltip-B1X8ECYn.js";
7
+ import { n as u } from "./error-banner-DmgOcFS3.js";
8
+ import { t as d } from "./tooltip-COXZxyl7.js";
9
9
  import { t as f } from "./debounce-C3jyATpc.js";
10
- import { j as p, n as ee } from "./useTheme-BlOgV7zf.js";
10
+ import { j as p, n as ee } from "./useTheme-Bm9_FyqO.js";
11
11
  import { c as m } from "./arrays-CN1djC4k.js";
12
12
  import { t as te } from "./objects-CzUgoMIF.js";
13
- import { t as h } from "./useAsyncData-BkvtR_Y2.js";
14
- import { a as ne, i as re, n as ie, t as g } from "./formats-CX_fkiT0.js";
13
+ import { t as h } from "./useAsyncData-x0_tWDWI.js";
14
+ import { a as ne, i as re, n as ie, t as g } from "./formats-DuRRWVwb.js";
15
15
  import { A as ae, n as oe } from "./vega-loader.browser-DEggZzni.js";
16
16
  import { t as se } from "./react-vega-DSD5aT7_.js";
17
- import { a as ce, n as _ } from "./loader-BIZus5lj.js";
18
- import { t as le } from "./useDeepCompareMemoize-DcZEHOmt.js";
17
+ import { a as ce, n as _ } from "./loader-VZ__bv4g.js";
18
+ import { t as le } from "./useDeepCompareMemoize-KaRHJEEU.js";
19
19
  import { t as ue } from "./semaphore-89jsoHtS.js";
20
20
  import { t as de } from "./use-vega-container-remeasure-DUW4tRxq.js";
21
21
  //#region src/plugins/impl/vega/fix-relative-url.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.24.1-dev113",
3
+ "version": "0.24.1-dev115",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -0,0 +1,151 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { describe, expect, it } from "vitest";
4
+ import type { CellId } from "@/core/cells/ids";
5
+ import type { Variable, VariableName, Variables } from "@/core/variables/types";
6
+ import { createCellRuntimeState } from "@/core/cells/types";
7
+ import {
8
+ collapsedNodeWidth,
9
+ computeDefsByCell,
10
+ isStateFlowVariable,
11
+ reusableCellIds,
12
+ wrapDefs,
13
+ } from "../elements";
14
+
15
+ const names = (list: string[]) => list as VariableName[];
16
+
17
+ function variable(
18
+ partial: Omit<Partial<Variable>, "name"> & { name: string },
19
+ ): Variable {
20
+ return {
21
+ declaredBy: [],
22
+ usedBy: [],
23
+ ...partial,
24
+ name: partial.name as VariableName,
25
+ };
26
+ }
27
+
28
+ describe("collapsedNodeWidth", () => {
29
+ it("clamps short labels to the minimum width", () => {
30
+ expect(collapsedNodeWidth("")).toBe(90);
31
+ expect(collapsedNodeWidth("x")).toBe(90);
32
+ });
33
+
34
+ it("clamps long labels to the maximum width", () => {
35
+ expect(collapsedNodeWidth("a".repeat(100))).toBe(240);
36
+ });
37
+
38
+ it("scales with label length between the bounds", () => {
39
+ const width = collapsedNodeWidth("df, summary");
40
+ expect(width).toBeGreaterThan(90);
41
+ expect(width).toBeLessThan(240);
42
+ });
43
+ });
44
+
45
+ describe("isStateFlowVariable", () => {
46
+ it("flags State and SetFunctor (from mo.state)", () => {
47
+ expect(
48
+ isStateFlowVariable(variable({ name: "g", dataType: "State" })),
49
+ ).toBe(true);
50
+ expect(
51
+ isStateFlowVariable(variable({ name: "s", dataType: "SetFunctor" })),
52
+ ).toBe(true);
53
+ });
54
+
55
+ it("does not flag UI elements or plain values", () => {
56
+ // UIElement is frontend-driven / immutable during a run — not a state flow.
57
+ expect(
58
+ isStateFlowVariable(variable({ name: "sl", dataType: "slider" })),
59
+ ).toBe(false);
60
+ expect(isStateFlowVariable(variable({ name: "n", dataType: "int" }))).toBe(
61
+ false,
62
+ );
63
+ expect(isStateFlowVariable(variable({ name: "u", dataType: null }))).toBe(
64
+ false,
65
+ );
66
+ expect(isStateFlowVariable(variable({ name: "u2" }))).toBe(false);
67
+ });
68
+ });
69
+
70
+ describe("wrapDefs", () => {
71
+ it("packs short names to the 12-char minimum budget", () => {
72
+ expect(
73
+ wrapDefs(names(["np", "math", "time", "io", "numpy", "pandas"])),
74
+ ).toEqual(["np, math", "time, io", "numpy", "pandas"]);
75
+ });
76
+
77
+ it("gives a long name its own line, then packs the rest to its width", () => {
78
+ expect(
79
+ wrapDefs(
80
+ names([
81
+ "very_very_very_long_variable_name",
82
+ "short_var",
83
+ "shr_v",
84
+ "io",
85
+ "numpy",
86
+ "wev",
87
+ ]),
88
+ ),
89
+ ).toEqual([
90
+ "very_very_very_long_variable_name",
91
+ "short_var, shr_v, io, numpy, wev",
92
+ ]);
93
+ });
94
+
95
+ it("returns an empty list for no defs", () => {
96
+ expect(wrapDefs(names([]))).toEqual([]);
97
+ });
98
+
99
+ it("caps the budget so packed lines fit the max node width", () => {
100
+ // One name far beyond the width cap must not widen every other line.
101
+ const huge = "x".repeat(60);
102
+ const lines = wrapDefs(
103
+ names([huge, "aaaaaaaaaa", "bbbbbbbbbb", "cccccccccc"]),
104
+ );
105
+ expect(lines[0]).toBe(huge);
106
+ for (const line of lines.slice(1)) {
107
+ expect(line.length).toBeLessThanOrEqual(33);
108
+ }
109
+ });
110
+ });
111
+
112
+ describe("computeDefsByCell", () => {
113
+ it("maps each cell to the variables it declares", () => {
114
+ const c1 = "c1" as CellId;
115
+ const c2 = "c2" as CellId;
116
+ const a = "a" as VariableName;
117
+ const b = "b" as VariableName;
118
+ const c = "c" as VariableName;
119
+ const variables: Variables = {
120
+ [a]: variable({ name: a, declaredBy: [c1], usedBy: [c2] }),
121
+ [b]: variable({ name: b, declaredBy: [c1] }),
122
+ [c]: variable({ name: c, declaredBy: [c2] }),
123
+ };
124
+ const defs = computeDefsByCell(variables);
125
+ expect(defs.get(c1)).toEqual([a, b]);
126
+ expect(defs.get(c2)).toEqual([c]);
127
+ });
128
+
129
+ it("returns an empty map when there are no variables", () => {
130
+ expect(computeDefsByCell({}).size).toBe(0);
131
+ });
132
+ });
133
+
134
+ describe("reusableCellIds", () => {
135
+ const c1 = "c1" as CellId;
136
+ const c2 = "c2" as CellId;
137
+ const c3 = "c3" as CellId;
138
+
139
+ it("collects cells whose serialization hint is valid, case-insensitively", () => {
140
+ const runtime = {
141
+ [c1]: createCellRuntimeState({ serialization: "Valid" }),
142
+ [c2]: createCellRuntimeState({ serialization: "Unparsable" }),
143
+ [c3]: createCellRuntimeState(),
144
+ };
145
+ expect(reusableCellIds([c1, c2, c3], runtime)).toEqual(new Set([c1]));
146
+ });
147
+
148
+ it("ignores cells with no runtime entry", () => {
149
+ expect(reusableCellIds([c1], {})).toEqual(new Set());
150
+ });
151
+ });
@@ -0,0 +1,68 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { memo } from "react";
4
+ import { BaseEdge, type EdgeProps, getBezierPath } from "reactflow";
5
+ import {
6
+ DEFAULT_EDGE_COLOR,
7
+ type DependencyEdgeData,
8
+ STATE_FLOW_EDGE_COLOR,
9
+ } from "./elements";
10
+
11
+ // Gentle curvature keeps edges reading as clean directional splines
12
+ // (TensorBoard-style) rather than hard right-angle elbows.
13
+ const EDGE_CURVATURE = 0.35;
14
+
15
+ /**
16
+ * A dependency edge, drawn as a smooth curved spline that flows out of the
17
+ * source handle and into the target — the TensorBoard graph aesthetic. Mutable
18
+ * `State`/setter flows get a distinct dashed accent style.
19
+ */
20
+ export const DependencyEdge = memo((props: EdgeProps<DependencyEdgeData>) => {
21
+ const {
22
+ sourceX,
23
+ sourceY,
24
+ targetX,
25
+ targetY,
26
+ sourcePosition,
27
+ targetPosition,
28
+ markerEnd,
29
+ data,
30
+ selected,
31
+ } = props;
32
+
33
+ const [path] = getBezierPath({
34
+ sourceX,
35
+ sourceY,
36
+ targetX,
37
+ targetY,
38
+ sourcePosition,
39
+ targetPosition,
40
+ curvature: EDGE_CURVATURE,
41
+ });
42
+
43
+ const isStateFlow = data?.isStateFlow ?? false;
44
+ // The inline stroke replaces React Flow's stylesheet-driven edge colors, so
45
+ // the selected state must be restyled here too or it becomes invisible.
46
+ const stroke = selected
47
+ ? "var(--primary)"
48
+ : isStateFlow
49
+ ? STATE_FLOW_EDGE_COLOR
50
+ : DEFAULT_EDGE_COLOR;
51
+
52
+ return (
53
+ <BaseEdge
54
+ path={path}
55
+ markerEnd={markerEnd}
56
+ style={{
57
+ strokeWidth: selected ? 2.5 : isStateFlow ? 2 : 1.5,
58
+ stroke,
59
+ strokeDasharray: isStateFlow ? "5 3" : undefined,
60
+ }}
61
+ />
62
+ );
63
+ });
64
+ DependencyEdge.displayName = "DependencyEdge";
65
+
66
+ export const edgeTypes = {
67
+ dependency: DependencyEdge,
68
+ };
@@ -2,25 +2,20 @@
2
2
 
3
3
  import { useAtomValue } from "jotai";
4
4
  import React, { memo, use } from "react";
5
- import { Handle, Position, useStore } from "reactflow";
5
+ import { Handle, Position } from "reactflow";
6
6
  import { TinyCode } from "@/components/editor/cell/TinyCode";
7
7
  import { useCellIds } from "@/core/cells/cells";
8
- import { displayCellName } from "@/core/cells/names";
8
+ import { displayCellName, isInternalCellName } from "@/core/cells/names";
9
9
  import { cn } from "@/utils/cn";
10
10
  import {
11
11
  type CustomNodeProps,
12
- getNodeHeight,
13
12
  INPUTS_HANDLE_ID,
13
+ nodeBodyLines,
14
+ nodeDimensions,
14
15
  OUTPUTS_HANDLE_ID,
15
16
  } from "./elements";
16
17
  import type { LayoutDirection } from "./types";
17
-
18
- function getWidth(canvasWidth: number) {
19
- const minWidth = 100;
20
- const maxWidth = 400;
21
- const padding = 50;
22
- return Math.min(Math.max(canvasWidth - padding * 2, minWidth), maxWidth);
23
- }
18
+ import { extractCellPreview } from "./utils/cell-preview";
24
19
 
25
20
  export const EdgeMarkerContext = React.createContext<LayoutDirection>("LR");
26
21
 
@@ -36,13 +31,19 @@ export const CustomNode = memo((props: CustomNodeProps) => {
36
31
  const { data, selected } = props; // must match the equality check
37
32
  const cell = useAtomValue(data.atom);
38
33
  const cellIndex = useCellIds().inOrderIds.indexOf(cell.id);
39
- const nonSelectedColor = "var(--gray-3)";
40
- const selectedColor = "var(--gray-9)";
41
- const color = selected ? selectedColor : nonSelectedColor;
42
- const reactFlowWidth = useStore(({ width }) => width);
34
+ const handleColor = selected ? "var(--gray-9)" : "var(--gray-3)";
43
35
  const edgeMarkers = use(EdgeMarkerContext);
44
36
 
45
- const linesOfCode = cell.code.split("\n").length;
37
+ const { width, height } = nodeDimensions(data);
38
+ const isNamed = !isInternalCellName(cell.name);
39
+ const name = displayCellName(cell.name, cellIndex);
40
+ // Only collapsed nodes without defs render the preview; skip the parse
41
+ // otherwise, since it scales with notebook size.
42
+ const preview =
43
+ !data.expanded && data.defs.length === 0
44
+ ? extractCellPreview(cell.code).text
45
+ : undefined;
46
+
46
47
  return (
47
48
  <div>
48
49
  <Handle
@@ -50,43 +51,70 @@ export const CustomNode = memo((props: CustomNodeProps) => {
50
51
  id={INPUTS_HANDLE_ID}
51
52
  data-testid="input-one"
52
53
  position={edgeMarkers === "LR" ? Position.Left : Position.Top}
53
- style={{ background: color }}
54
+ style={{ background: handleColor }}
54
55
  />
55
56
  <Handle
56
57
  type="source"
57
58
  id={INPUTS_HANDLE_ID}
58
59
  data-testid="input-two"
59
60
  position={edgeMarkers === "LR" ? Position.Left : Position.Top}
60
- style={{ background: color }}
61
+ style={{ background: handleColor }}
61
62
  />
62
63
  <div
63
64
  className={cn(
64
65
  "flex flex-col bg-card border border-input/50 rounded-md mx-[2px] overflow-hidden",
65
66
  selected && "border-primary",
66
67
  )}
67
- style={{
68
- height: getNodeHeight(linesOfCode),
69
- width: data.forceWidth || getWidth(reactFlowWidth),
70
- }}
68
+ style={{ height, width }}
71
69
  >
72
- <div className="text-muted-foreground font-semibold text-xs py-1 px-2 bg-muted border-b">
73
- {displayCellName(cell.name, cellIndex)}
70
+ <div className="text-xs py-0.5 px-2 bg-muted border-b flex items-center justify-between gap-2 shrink-0">
71
+ <span
72
+ className={cn(
73
+ "truncate",
74
+ isNamed
75
+ ? "font-semibold text-foreground"
76
+ : "text-muted-foreground/70",
77
+ )}
78
+ >
79
+ {name}
80
+ </span>
74
81
  </div>
75
- <TinyCode code={cell.code} />
82
+ {data.expanded ? (
83
+ <TinyCode code={cell.code} />
84
+ ) : (
85
+ <div className="flex-1 min-h-0 flex flex-col justify-center px-2 py-1 font-mono text-xs leading-4">
86
+ {data.defs.length > 0 ? (
87
+ // Defs wrapped into comma-separated lines — taller, not wider.
88
+ nodeBodyLines(data).map((line) => (
89
+ <span
90
+ key={line}
91
+ className="text-foreground truncate"
92
+ title={line}
93
+ >
94
+ {line}
95
+ </span>
96
+ ))
97
+ ) : (
98
+ <span className="truncate italic text-muted-foreground/60">
99
+ {preview ?? "empty"}
100
+ </span>
101
+ )}
102
+ </div>
103
+ )}
76
104
  </div>
77
105
  <Handle
78
106
  type="source"
79
107
  id={OUTPUTS_HANDLE_ID}
80
108
  data-testid="output-one"
81
109
  position={edgeMarkers === "LR" ? Position.Right : Position.Bottom}
82
- style={{ background: color }}
110
+ style={{ background: handleColor }}
83
111
  />
84
112
  <Handle
85
113
  type="target"
86
114
  id={OUTPUTS_HANDLE_ID}
87
115
  data-testid="output-two"
88
116
  position={edgeMarkers === "LR" ? Position.Right : Position.Bottom}
89
- style={{ background: color }}
117
+ style={{ background: handleColor }}
90
118
  />
91
119
  </div>
92
120
  );