@marimo-team/islands 0.21.1-dev12 → 0.21.1-dev14

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.
@@ -9,7 +9,7 @@ import { a as cva, g as Logger, y as cn } from "./button-DQpBib29.js";
9
9
  import { t as require_jsx_runtime } from "./jsx-runtime-CTBg5pdT.js";
10
10
  import { r as KnownQueryParams } from "./constants-CytQ_3LM.js";
11
11
  import { f as waitFor, p as isIslands, u as store, y as atom } from "./useTheme-D0rdoMBF.js";
12
- import { i as tableFromIPC } from "./loader-DsE3MiYo.js";
12
+ import { i as tableFromIPC } from "./loader-V1UqqlAy.js";
13
13
  var CircleQuestionMark = createLucideIcon("circle-question-mark", [
14
14
  ["circle", {
15
15
  cx: "12",
@@ -1,7 +1,7 @@
1
1
  import { s as __toESM } from "./chunk-BNovOVIE.js";
2
2
  import { t as require_react } from "./react-Bs6Z0kvn.js";
3
3
  import { t as require_compiler_runtime } from "./compiler-runtime-B_OLMU9S.js";
4
- import { S as CircleQuestionMark, a as AlertTitle, m as asRemoteURL, n as arrow, o as isValid, r as Alert, t as useDeepCompareMemoize } from "./useDeepCompareMemoize-DLS-bHHT.js";
4
+ import { S as CircleQuestionMark, a as AlertTitle, m as asRemoteURL, n as arrow, o as isValid, r as Alert, t as useDeepCompareMemoize } from "./useDeepCompareMemoize-CJr2MRe6.js";
5
5
  import { d as Objects, g as Logger, h as Events, y as cn } from "./button-DQpBib29.js";
6
6
  import "./Combination-Dk6JxauT.js";
7
7
  import { t as require_jsx_runtime } from "./jsx-runtime-CTBg5pdT.js";
@@ -9,7 +9,7 @@ import "./react-dom-CqtLRVZP.js";
9
9
  import { t as Tooltip } from "./tooltip-SPkubVH3.js";
10
10
  import { i as debounce_default } from "./constants-CytQ_3LM.js";
11
11
  import { C as useEvent_default, n as useTheme } from "./useTheme-D0rdoMBF.js";
12
- import { a as tooltipHandler, n as vegaLoadData } from "./loader-DsE3MiYo.js";
12
+ import { a as getContainerWidth, n as vegaLoadData, s as tooltipHandler } from "./loader-V1UqqlAy.js";
13
13
  import { t as uniq_default } from "./uniq-H2E5nMLq.js";
14
14
  import { n as ErrorBanner } from "./error-banner-BctofTCP.js";
15
15
  import { n as formats } from "./vega-loader.browser-CQ-lnUkI.js";
@@ -598,9 +598,12 @@ var VegaComponent = (e) => {
598
598
  children: B.stack
599
599
  })]
600
600
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
601
- className: cn("relative", "width" in W && W.width === "container" && "vega-container-width"),
601
+ className: cn("relative"),
602
602
  onPointerDown: Events.stopPropagation(),
603
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ref: R }), Q()]
603
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
604
+ ref: R,
605
+ "data-container-width": getContainerWidth(W)
606
+ }), Q()]
604
607
  })] });
605
608
  };
606
609
  function convertSetToList(e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.21.1-dev12",
3
+ "version": "0.21.1-dev14",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -4,6 +4,7 @@ import React from "react";
4
4
  import type { TopLevelSpec } from "vega-lite";
5
5
  import { LazyVegaEmbed } from "@/components/charts/lazy";
6
6
  import { tooltipHandler } from "@/components/charts/tooltip";
7
+ import { getContainerWidth } from "@/plugins/impl/vega/utils";
7
8
  import { useTheme } from "@/theme/useTheme";
8
9
  import type { ErrorMessage } from "./chart-spec/spec";
9
10
  import { augmentSpecWithData } from "./chart-spec/spec";
@@ -30,6 +31,7 @@ export const LazyChart: React.FC<{
30
31
  <React.Suspense fallback={<LoadingChart />}>
31
32
  <LazyVegaEmbed
32
33
  spec={spec}
34
+ data-container-width={getContainerWidth(spec)}
33
35
  options={{
34
36
  theme: theme === "dark" ? "dark" : undefined,
35
37
  height: height,
@@ -45,6 +45,7 @@ export const TableColumnSummary = <TData, TValue>({
45
45
  <Suspense fallback={skeleton}>
46
46
  <LazyVegaEmbed
47
47
  spec={spec}
48
+ data-container-width="container"
48
49
  options={{
49
50
  width: 80,
50
51
  height: 30,
@@ -224,6 +224,7 @@ export function renderChart(chartSpec: string, theme: Theme) {
224
224
  return (
225
225
  <Suspense fallback={LoadingChart}>
226
226
  <LazyVegaEmbed
227
+ data-container-width="container"
227
228
  spec={updateSpec(JSON.parse(chartSpec) as TopLevelFacetedUnitSpec)}
228
229
  options={{
229
230
  theme: theme === "dark" ? "dark" : "vox",
@@ -27,6 +27,7 @@ import { useOverflowDetection } from "@/hooks/useOverflowDetection";
27
27
  import { renderHTML } from "@/plugins/core/RenderHTML";
28
28
  import { Banner } from "@/plugins/impl/common/error-banner";
29
29
  import type { TopLevelFacetedUnitSpec } from "@/plugins/impl/data-explorer/queries/types";
30
+ import { getContainerWidth } from "@/plugins/impl/vega/utils";
30
31
  import { useTheme } from "@/theme/useTheme";
31
32
  import { Events } from "@/utils/events";
32
33
  import { invariant } from "@/utils/invariant";
@@ -205,6 +206,7 @@ export const OutputRenderer: React.FC<{
205
206
  <Suspense fallback={<ChartLoadingState />}>
206
207
  <LazyVegaEmbed
207
208
  spec={parsedJsonData as TopLevelFacetedUnitSpec}
209
+ data-container-width={getContainerWidth(parsedJsonData)}
208
210
  options={{
209
211
  theme: theme === "dark" ? "dark" : undefined,
210
212
  mode: "vega-lite",
@@ -5,6 +5,7 @@ import { useAtom } from "jotai";
5
5
  import { CrosshairIcon, PinIcon, PinOffIcon, XIcon } from "lucide-react";
6
6
  import type { PropsWithChildren } from "react";
7
7
  import { Panel, PanelResizeHandle } from "react-resizable-panels";
8
+ import { raf2 } from "@/components/editor/navigation/focus-utils";
8
9
  import { Button } from "@/components/ui/button";
9
10
  import { Toggle } from "@/components/ui/toggle";
10
11
  import { Tooltip } from "@/components/ui/tooltip";
@@ -151,6 +152,11 @@ const ResizableComponent = ({ children }: ResizableComponentProps) => {
151
152
  startingWidth: 400,
152
153
  minWidth: 300,
153
154
  maxWidth: 1500,
155
+ onResize: () => {
156
+ raf2(() => {
157
+ window.dispatchEvent(new Event("resize"));
158
+ });
159
+ },
154
160
  });
155
161
 
156
162
  return (
@@ -0,0 +1,35 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import { handleDragging } from "../utils";
5
+
6
+ describe("handleDragging", () => {
7
+ it("should dispatch a resize event after dragging ends", async () => {
8
+ const listener = vi.fn();
9
+ window.addEventListener("resize", listener);
10
+
11
+ handleDragging(false);
12
+
13
+ // raf2: wait two animation frames
14
+ await new Promise((resolve) => requestAnimationFrame(resolve));
15
+ await new Promise((resolve) => requestAnimationFrame(resolve));
16
+
17
+ expect(listener).toHaveBeenCalledTimes(1);
18
+
19
+ window.removeEventListener("resize", listener);
20
+ });
21
+
22
+ it("should not dispatch a resize event while dragging", async () => {
23
+ const listener = vi.fn();
24
+ window.addEventListener("resize", listener);
25
+
26
+ handleDragging(true);
27
+
28
+ await new Promise((resolve) => requestAnimationFrame(resolve));
29
+ await new Promise((resolve) => requestAnimationFrame(resolve));
30
+
31
+ expect(listener).not.toHaveBeenCalled();
32
+
33
+ window.removeEventListener("resize", listener);
34
+ });
35
+ });
@@ -17,6 +17,7 @@ import "./app-chrome.css";
17
17
  import { TooltipProvider } from "@radix-ui/react-tooltip";
18
18
  import { useAtom, useAtomValue } from "jotai";
19
19
  import { XIcon } from "lucide-react";
20
+ import useEvent from "react-use-event-hook";
20
21
  import { Button } from "@/components/ui/button";
21
22
  import { ReorderableList } from "@/components/ui/reorderable-list";
22
23
  import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
@@ -26,6 +27,7 @@ import { capabilitiesAtom } from "@/core/config/capabilities";
26
27
  import { getFeatureFlag } from "@/core/config/feature-flag";
27
28
  import { cn } from "@/utils/cn";
28
29
  import { ErrorBoundary } from "../../boundary/ErrorBoundary";
30
+ import { raf2 } from "../../navigation/focus-utils";
29
31
  import { ContextAwarePanel } from "../panels/context-aware-panel/context-aware-panel";
30
32
  import { PanelSectionProvider } from "../panels/panel-context";
31
33
  import { panelLayoutAtom, useChromeActions, useChromeState } from "../state";
@@ -147,6 +149,14 @@ export const AppChrome: React.FC<PropsWithChildren> = ({ children }) => {
147
149
  });
148
150
  }, [panelLayout.sidebar, capabilities]);
149
151
 
152
+ const emitResizeEvent = useEvent(() => {
153
+ // HACK: Unfortunately, we have to do this twice to make sure the
154
+ // panel is fully expanded before we dispatch the resize event
155
+ raf2(() => {
156
+ window.dispatchEvent(new Event("resize"));
157
+ });
158
+ });
159
+
150
160
  // sync sidebar
151
161
  useEffect(() => {
152
162
  if (!sidebarRef.current) {
@@ -162,13 +172,7 @@ export const AppChrome: React.FC<PropsWithChildren> = ({ children }) => {
162
172
  }
163
173
 
164
174
  // Dispatch a resize event so widgets know to resize
165
- requestAnimationFrame(() => {
166
- // HACK: Unfortunately, we have to do this twice to make sure it the
167
- // panel is fully expanded before we dispatch the resize event
168
- requestAnimationFrame(() => {
169
- window.dispatchEvent(new Event("resize"));
170
- });
171
- });
175
+ emitResizeEvent();
172
176
  }, [isSidebarOpen]);
173
177
 
174
178
  // sync panel
@@ -186,13 +190,7 @@ export const AppChrome: React.FC<PropsWithChildren> = ({ children }) => {
186
190
  }
187
191
 
188
192
  // Dispatch a resize event so widgets know to resize
189
- requestAnimationFrame(() => {
190
- // HACK: Unfortunately, we have to do this twice to make sure it the
191
- // panel is fully expanded before we dispatch the resize event
192
- requestAnimationFrame(() => {
193
- window.dispatchEvent(new Event("resize"));
194
- });
195
- });
193
+ emitResizeEvent();
196
194
  }, [isDeveloperPanelOpen]);
197
195
 
198
196
  // Auto-correct developer panel selection when the selected tab is no longer available
@@ -1,8 +1,12 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
+ import { raf2 } from "../../navigation/focus-utils";
4
+
3
5
  export function handleDragging(isDragging: boolean) {
4
6
  if (!isDragging) {
5
7
  // Once the user is done dragging, dispatch a resize event
6
- window.dispatchEvent(new Event("resize"));
8
+ raf2(() => {
9
+ window.dispatchEvent(new Event("resize"));
10
+ });
7
11
  }
8
12
  }
@@ -1115,6 +1115,60 @@ class Foo:
1115
1115
  `);
1116
1116
  });
1117
1117
 
1118
+ test("should not highlight attribute access matching a for-loop variable", () => {
1119
+ // When `tool` is used as a for-loop variable, `mcp.tool` (attribute access)
1120
+ // should NOT have `tool` highlighted — it's a property, not a variable reference.
1121
+ expect(
1122
+ runHighlight(
1123
+ ["mcp", "client"],
1124
+ `
1125
+ @mcp.tool
1126
+ def roll_dice():
1127
+ pass
1128
+
1129
+ async with client:
1130
+ for tool in await client.list_tools():
1131
+ print(tool)
1132
+ `,
1133
+ ),
1134
+ ).toMatchInlineSnapshot(`
1135
+ "
1136
+ @mcp.tool
1137
+ ^^^
1138
+ def roll_dice():
1139
+ pass
1140
+
1141
+ async with client:
1142
+ ^^^^^^
1143
+ for tool in await client.list_tools():
1144
+ ^^^^^^
1145
+ print(tool)
1146
+ "
1147
+ `);
1148
+ });
1149
+
1150
+ test("should not highlight property name matching a global variable", () => {
1151
+ // `tool` is a global variable, but `mcp.tool` should not highlight `tool`
1152
+ // because it's a property access, not a variable reference.
1153
+ expect(
1154
+ runHighlight(
1155
+ ["mcp", "tool"],
1156
+ `
1157
+ @mcp.tool
1158
+ def roll_dice():
1159
+ pass
1160
+ `,
1161
+ ),
1162
+ ).toMatchInlineSnapshot(`
1163
+ "
1164
+ @mcp.tool
1165
+ ^^^
1166
+ def roll_dice():
1167
+ pass
1168
+ "
1169
+ `);
1170
+ });
1171
+
1118
1172
  test("class property self-reference", () => {
1119
1173
  expect(
1120
1174
  runHighlight(
@@ -462,6 +462,7 @@ export function findReactiveVariables(options: {
462
462
  ): boolean {
463
463
  return (
464
464
  allVariableNames.has(varName) &&
465
+ !isPropertyAccess(cursor) &&
465
466
  !isKeywordArgumentName(cursor) &&
466
467
  !isAssignmentTarget(cursor)
467
468
  );
@@ -529,6 +530,19 @@ function isAssignmentTarget(cursor: TreeCursor): boolean {
529
530
  return false;
530
531
  }
531
532
 
533
+ /** Checks whether a `VariableName` is a property access (e.g. `tool` in `mcp.tool`). */
534
+ function isPropertyAccess(cursor: TreeCursor): boolean {
535
+ // Check if the previous sibling is a "." node, which means this
536
+ // VariableName is a property access rather than a standalone variable.
537
+ // This handles both MemberExpression (e.g. `obj.attr`) and Decorator
538
+ // (e.g. `@mcp.tool`) where the parser emits flat sibling nodes.
539
+ const temp = cursor.node.cursor();
540
+ if (temp.prevSibling() && temp.name === ".") {
541
+ return true;
542
+ }
543
+ return false;
544
+ }
545
+
532
546
  /**
533
547
  * Checks if the syntax tree contains any syntax errors.
534
548
  * If there are errors, we shouldn't show reactive variable highlighting.
@@ -17,6 +17,7 @@ import { cn } from "@/utils/cn";
17
17
  import { Objects } from "@/utils/objects";
18
18
  import { ErrorBanner } from "../common/error-banner";
19
19
  import { vegaLoadData } from "../vega/loader";
20
+ import { getContainerWidth } from "../vega/utils";
20
21
  import { ColumnSummary } from "./components/column-summary";
21
22
  import { QueryForm } from "./components/query-form";
22
23
  import type { SpecificEncoding } from "./encoding";
@@ -140,16 +141,18 @@ export const DataExplorerComponent = ({
140
141
  const responsiveSpec = makeResponsive(spec);
141
142
  // TODO: We can optimize by updating the data dynamically. https://github.com/vega/react-vega?tab=readme-ov-file#recipes
142
143
  const augmentedSpec = augmentSpecWithData(responsiveSpec, chartData);
143
- const isContainerWidth = responsiveSpec.width === "container";
144
144
 
145
145
  return (
146
146
  <div
147
147
  className={cn(
148
148
  "flex overflow-y-auto justify-center items-center flex-1 w-[90%]",
149
- isContainerWidth && "vega-container-width",
150
149
  )}
151
150
  >
152
- <VegaEmbed spec={augmentedSpec} options={chartOptions(theme)} />
151
+ <VegaEmbed
152
+ data-container-width={getContainerWidth(augmentedSpec)}
153
+ spec={augmentedSpec}
154
+ options={chartOptions(theme)}
155
+ />
153
156
  </div>
154
157
  );
155
158
  };
@@ -0,0 +1,36 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { describe, expect, it } from "vitest";
4
+ import { getContainerWidth } from "../utils";
5
+
6
+ describe("getContainerWidth", () => {
7
+ it('should return "container" when spec width is "container"', () => {
8
+ expect(getContainerWidth({ width: "container" })).toBe("container");
9
+ });
10
+
11
+ it("should return a numeric width", () => {
12
+ expect(getContainerWidth({ width: 500 })).toBe(500);
13
+ });
14
+
15
+ it("should return undefined when spec has no width", () => {
16
+ expect(getContainerWidth({ height: 300 })).toBeUndefined();
17
+ });
18
+
19
+ it("should return undefined for null", () => {
20
+ expect(getContainerWidth(null)).toBeUndefined();
21
+ });
22
+
23
+ it("should return undefined for undefined", () => {
24
+ expect(getContainerWidth(undefined)).toBeUndefined();
25
+ });
26
+
27
+ it("should return undefined for non-object values", () => {
28
+ expect(getContainerWidth("string")).toBeUndefined();
29
+ expect(getContainerWidth(42)).toBeUndefined();
30
+ expect(getContainerWidth(true)).toBeUndefined();
31
+ });
32
+
33
+ it("should return undefined when width is explicitly undefined", () => {
34
+ expect(getContainerWidth({ width: undefined })).toBeUndefined();
35
+ });
36
+ });
@@ -3,6 +3,20 @@
3
3
  import { Objects } from "@/utils/objects";
4
4
  import type { DataType, FieldTypes, VegaDataType } from "./vega-loader";
5
5
 
6
+ export type ContainerWidth = number | "container";
7
+
8
+ /**
9
+ * Get the container width from a VegaLite spec.
10
+ * @param spec - The VegaLite spec.
11
+ * @returns The container width.
12
+ */
13
+ export function getContainerWidth(spec: unknown): ContainerWidth | undefined {
14
+ if (typeof spec === "object" && spec !== null && "width" in spec) {
15
+ return spec.width as ContainerWidth | undefined;
16
+ }
17
+ return undefined;
18
+ }
19
+
6
20
  export function mergeAsArrays<T>(
7
21
  left: T | T[] | undefined,
8
22
  right: T | T[] | undefined,
@@ -27,6 +27,7 @@ import { makeSelectable } from "./make-selectable";
27
27
  import { getSelectionParamNames, ParamNames } from "./params";
28
28
  import { resolveVegaSpecData } from "./resolve-data";
29
29
  import type { VegaLiteSpec } from "./types";
30
+ import { getContainerWidth } from "./utils";
30
31
 
31
32
  // register arrow reader under type 'arrow'
32
33
  formats("arrow", arrow);
@@ -305,16 +306,14 @@ const LoadedVegaComponent = ({
305
306
  </Alert>
306
307
  )}
307
308
  <div
308
- className={cn(
309
- "relative",
310
- "width" in selectableSpec &&
311
- selectableSpec.width === "container" &&
312
- "vega-container-width",
313
- )}
309
+ className={cn("relative")}
314
310
  // Capture the pointer down event to prevent the parent from handling it
315
311
  onPointerDown={Events.stopPropagation()}
316
312
  >
317
- <div ref={vegaRef} />
313
+ <div
314
+ ref={vegaRef}
315
+ data-container-width={getContainerWidth(selectableSpec)}
316
+ />
318
317
  {renderHelpContent()}
319
318
  </div>
320
319
  </>
@@ -4,7 +4,7 @@
4
4
  max-width: 100%;
5
5
  }
6
6
 
7
- .vega-container-width .vega-embed {
7
+ .vega-embed[data-container-width="container"] {
8
8
  width: 100%;
9
9
  }
10
10