@marimo-team/islands 0.23.15-dev33 → 0.23.15-dev4

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 (85) hide show
  1. package/dist/{ConnectedDataExplorerComponent-7p7rt9iw.js → ConnectedDataExplorerComponent-CU4fZJzG.js} +4 -4
  2. package/dist/{ErrorBoundary-B_CAG7_e.js → ErrorBoundary-DE6tzZf-.js} +1 -1
  3. package/dist/{ImageComparisonComponent-1i5IBhon.js → ImageComparisonComponent-CHrI72em.js} +129 -173
  4. package/dist/assets/__vite-browser-external-BQCLNwri.js +1 -0
  5. package/dist/assets/worker-DAWRHcPq.js +73 -0
  6. package/dist/{chat-ui-C0wkSwxB.js → chat-ui-DinOvbWu.js} +3111 -3111
  7. package/dist/{code-visibility-BI94mK4u.js → code-visibility-Dk8cVOny.js} +864 -946
  8. package/dist/{debounce-BOD3DbfP.js → constants-T20xxyNf.js} +24 -1
  9. package/dist/{formats-BX3uDQbB.js → formats-Dzx4J_z1.js} +1 -1
  10. package/dist/{glide-data-editor-CwZz71BD.js → glide-data-editor-CjTu7ukN.js} +3 -3
  11. package/dist/{html-to-image-D8Q5SLSJ.js → html-to-image-_wGfk8V-.js} +2241 -2277
  12. package/dist/{input-BGPrFH3g.js → input-DtsN7xm-.js} +2 -2
  13. package/dist/main.js +1201 -1297
  14. package/dist/{mermaid-UdmxG2PZ.js → mermaid-BYqXy_NE.js} +2 -2
  15. package/dist/{process-output-C_A12w9J.js → process-output-Mh4UrjwM.js} +1 -1
  16. package/dist/{reveal-component-ELidGmPF.js → reveal-component-B5DXLyO7.js} +605 -618
  17. package/dist/{spec-DSs9v0xx.js → spec-Cz-Bj1JI.js} +1 -1
  18. package/dist/style.css +1 -1
  19. package/dist/{toDate-CKpRx4TS.js → toDate-CWNNlFEX.js} +4 -3
  20. package/dist/{useAsyncData-BSOyAbac.js → useAsyncData-KfHB8wQR.js} +1 -1
  21. package/dist/{useDeepCompareMemoize-BTLeWzuR.js → useDeepCompareMemoize-nJMtxhm4.js} +1 -1
  22. package/dist/{useLifecycle-C6wHjkhW.js → useLifecycle-DegSo0lV.js} +1 -1
  23. package/dist/{useTheme-Df_vGflw.js → useTheme-6eZ3GOTS.js} +27 -62
  24. package/dist/{vega-component-DNHEV0u0.js → vega-component-DzyyM9fc.js} +6 -6
  25. package/package.json +1 -1
  26. package/src/components/chat/__tests__/chat-utils.test.ts +1 -244
  27. package/src/components/chat/chat-panel.tsx +67 -196
  28. package/src/components/chat/chat-utils.ts +2 -111
  29. package/src/components/editor/SortableCell.tsx +2 -6
  30. package/src/components/editor/actions/pair-with-agent-modal.tsx +64 -20
  31. package/src/components/editor/ai/add-cell-with-ai.tsx +5 -14
  32. package/src/components/editor/ai/ai-completion-editor.tsx +1 -4
  33. package/src/components/editor/cell/cell-context-menu.tsx +0 -7
  34. package/src/components/editor/chrome/panels/packages-panel.tsx +1 -11
  35. package/src/components/editor/columns/cell-column.tsx +4 -34
  36. package/src/components/editor/columns/sortable-column.tsx +4 -24
  37. package/src/components/editor/navigation/__tests__/navigation.test.ts +0 -33
  38. package/src/components/editor/navigation/navigation.ts +1 -8
  39. package/src/components/editor/notebook-cell.tsx +106 -203
  40. package/src/components/editor/renderers/cell-array.tsx +13 -26
  41. package/src/components/editor/renderers/cells-renderer.tsx +2 -8
  42. package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +0 -29
  43. package/src/components/editor/renderers/slides-layout/types.ts +0 -4
  44. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +19 -19
  45. package/src/components/slides/reveal-component.tsx +4 -37
  46. package/src/components/slides/slide-form.tsx +0 -72
  47. package/src/core/cells/__tests__/utils.test.ts +0 -59
  48. package/src/core/cells/utils.ts +0 -51
  49. package/src/core/codemirror/completion/__tests__/signature-hint.test.ts +3 -97
  50. package/src/core/codemirror/completion/signature-hint.ts +11 -78
  51. package/src/core/constants.ts +0 -6
  52. package/src/core/islands/__tests__/bridge.test.ts +46 -341
  53. package/src/core/islands/__tests__/parse.test.ts +0 -105
  54. package/src/core/islands/bootstrap.ts +3 -8
  55. package/src/core/islands/bridge.ts +23 -116
  56. package/src/core/islands/components/web-components.tsx +15 -76
  57. package/src/core/islands/constants.ts +0 -1
  58. package/src/core/islands/main.ts +3 -69
  59. package/src/core/islands/parse.ts +23 -70
  60. package/src/core/islands/worker/worker.tsx +57 -145
  61. package/src/core/wasm/worker/bootstrap.ts +20 -113
  62. package/src/css/app/Cell.css +0 -10
  63. package/src/hooks/useHotkey.ts +4 -29
  64. package/src/plugins/impl/anywidget/__tests__/host.test.ts +9 -6
  65. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +61 -22
  66. package/src/plugins/impl/anywidget/model-proxy.ts +13 -0
  67. package/src/plugins/impl/anywidget/widget-binding.ts +34 -4
  68. package/src/plugins/impl/image-comparison/ImageComparisonComponent.tsx +2 -53
  69. package/src/plugins/impl/matplotlib/__tests__/matplotlib-renderer.test.ts +3 -71
  70. package/src/plugins/impl/matplotlib/matplotlib-renderer.ts +0 -1
  71. package/src/theme/useTheme.ts +1 -16
  72. package/dist/assets/__vite-browser-external-CjNAy01L.js +0 -1
  73. package/dist/assets/worker-B3XPCb6y.js +0 -98
  74. package/src/components/chat/__tests__/message-queue.test.tsx +0 -121
  75. package/src/components/editor/__tests__/output-persistence.test.tsx +0 -241
  76. package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +0 -153
  77. package/src/components/editor/actions/pair-with-agent-commands.ts +0 -109
  78. package/src/components/editor/columns/__tests__/cell-column.test.tsx +0 -105
  79. package/src/components/editor/renderers/__tests__/cells-renderer.test.tsx +0 -66
  80. package/src/core/islands/__tests__/main.test.ts +0 -176
  81. package/src/core/islands/components/__tests__/web-components.test.tsx +0 -214
  82. package/src/core/islands/worker/__tests__/controller.test.ts +0 -173
  83. package/src/hooks/__tests__/useHotkey.test.tsx +0 -88
  84. package/src/plugins/impl/image-comparison/__tests__/ImageComparisonComponent.test.tsx +0 -71
  85. package/src/theme/__tests__/useTheme.test.ts +0 -68
@@ -17,31 +17,13 @@ type HotkeyHandler = (
17
17
  // oxlint-disable-next-line typescript/no-invalid-void-type
18
18
  ) => boolean | void | undefined | Promise<void>;
19
19
 
20
- interface HotkeyOptions {
21
- /**
22
- * If true, the hotkey is not handled at all: the callback does not run, the
23
- * event's default is not prevented, and the action is not registered in the
24
- * shortcut registry.
25
- *
26
- * This differs from passing `Functions.NOOP` as the callback, which still
27
- * swallows the keystroke (preventDefault) to suppress native browser/OS
28
- * behavior.
29
- */
30
- disabled?: boolean;
31
- }
32
-
33
20
  /**
34
21
  * Registers a hotkey listener for the given shortcut.
35
22
  *
36
23
  * @param callback The callback to run when the shortcut is pressed. It does not need to be memoized as this hook will always use the latest callback.
37
24
  * If the callback returns false, preventDefault will not be called on the event.
38
25
  */
39
- export function useHotkey(
40
- shortcut: HotkeyAction,
41
- callback: HotkeyHandler,
42
- options: HotkeyOptions = {},
43
- ) {
44
- const { disabled = false } = options;
26
+ export function useHotkey(shortcut: HotkeyAction, callback: HotkeyHandler) {
45
27
  const { registerAction, unregisterAction } = useSetRegisteredAction();
46
28
  const hotkeys = useAtomValue(hotkeysAtom);
47
29
 
@@ -49,7 +31,7 @@ export function useHotkey(
49
31
  const memoizeCallback = useEvent((evt?: KeyboardEvent) => callback(evt));
50
32
 
51
33
  const listener = useEvent((e: KeyboardEvent) => {
52
- if (disabled || e.defaultPrevented) {
34
+ if (e.defaultPrevented) {
53
35
  return;
54
36
  }
55
37
 
@@ -69,18 +51,11 @@ export function useHotkey(
69
51
 
70
52
  // Register with the shortcut registry
71
53
  useEffect(() => {
72
- if (!isNOOP && !disabled) {
54
+ if (!isNOOP) {
73
55
  registerAction(shortcut, memoizeCallback);
74
56
  return () => unregisterAction(shortcut);
75
57
  }
76
- }, [
77
- memoizeCallback,
78
- shortcut,
79
- isNOOP,
80
- disabled,
81
- registerAction,
82
- unregisterAction,
83
- ]);
58
+ }, [memoizeCallback, shortcut, isNOOP, registerAction, unregisterAction]);
84
59
  }
85
60
 
86
61
  /**
@@ -277,22 +277,25 @@ describe("WidgetBinding receives a host in render props", () => {
277
277
  });
278
278
  });
279
279
 
280
- describe("host-mounted views", () => {
281
- it("render with the child's current model state", async () => {
282
- const childId = asModelId("host-child-state");
280
+ describe("host-mounted views hydrate like display-mounted ones", () => {
281
+ it("replays current state to a child's render listeners", async () => {
282
+ const childId = asModelId("host-hydration-child");
283
283
  const parentController = new AbortController();
284
284
  const getModuleSpy = vi.spyOn(WIDGET_DEF_REGISTRY, "getModule");
285
285
  try {
286
286
  const childWidget = {
287
287
  render: vi.fn(({ model, el }) => {
288
- el.textContent = `count is ${model.get("count")}`;
288
+ el.textContent = "count is 5";
289
+ model.on("change:count", () => {
290
+ el.textContent = `count is ${model.get("count")}`;
291
+ });
289
292
  }),
290
293
  };
291
294
  const childModel = new Model<ModelState>({ count: 8 }, createMockComm());
292
295
  WIDGET_REGISTRY.setModel(childId, childModel);
293
296
  WIDGET_REGISTRY.setSpec(childId, {
294
- url: "./@file/10-host-child-state.js",
295
- hash: "hash-host-child-state",
297
+ url: "./@file/10-host-hydration-child.js",
298
+ hash: "hash-host-hydration-child",
296
299
  });
297
300
  getModuleSpy.mockResolvedValue({ default: childWidget });
298
301
 
@@ -337,12 +337,13 @@ describe("WidgetBinding", () => {
337
337
  { el: document.createElement("div") },
338
338
  { signal: controller.signal },
339
339
  );
340
+ // Called once by the hydration replay at mount, once by the set.
340
341
  model.set("count", 1);
341
- expect(onCount).toHaveBeenCalledTimes(1);
342
+ expect(onCount).toHaveBeenCalledTimes(2);
342
343
 
343
344
  controller.abort();
344
345
  model.set("count", 2);
345
- expect(onCount).toHaveBeenCalledTimes(1);
346
+ expect(onCount).toHaveBeenCalledTimes(2);
346
347
  });
347
348
 
348
349
  it("should auto-clear initialize listeners on destroy", async () => {
@@ -365,32 +366,30 @@ describe("WidgetBinding", () => {
365
366
  });
366
367
  });
367
368
 
368
- describe("WidgetBinding render listeners", () => {
369
- it("fire only for changes after mount, never for pre-existing state", async () => {
369
+ describe("WidgetBinding hydration replay", () => {
370
+ it("replays current state to render listeners exactly once", async () => {
370
371
  const model = new Model<ModelState>({ count: 8 }, createMockComm());
371
- const onCount = vi.fn();
372
- const onAnyChange = vi.fn();
372
+ const el = document.createElement("div");
373
373
  const widgetDef = {
374
- render: vi.fn(({ model }) => {
375
- model.on("change:count", onCount);
376
- model.on("change", onAnyChange);
374
+ render: vi.fn(({ model, el }) => {
375
+ // A widget view that starts with a local default and relies on
376
+ // change events for hydration.
377
+ el.textContent = "count is 5";
378
+ model.on("change:count", () => {
379
+ el.textContent = `count is ${model.get("count")}`;
380
+ });
377
381
  }),
378
382
  };
379
383
  const binding = await createBinding(widgetDef, model);
380
- const controller = new AbortController();
381
- await binding.createView(
382
- { el: document.createElement("div") },
383
- { signal: controller.signal },
384
- );
385
- expect(onCount).not.toHaveBeenCalled();
386
- expect(onAnyChange).not.toHaveBeenCalled();
387
384
 
388
- model.set("count", 9);
389
- expect(onCount).toHaveBeenCalledTimes(1);
390
- expect(onCount).toHaveBeenCalledWith(9);
385
+ const controller = new AbortController();
386
+ await binding.createView({ el }, { signal: controller.signal });
387
+ expect(el.textContent).toBe("count is 8");
391
388
  });
392
389
 
393
- it("mounting a second view does not fire the first view's listeners", async () => {
390
+ it("does not re-fire listeners of already-mounted views", async () => {
391
+ // Mounting view B must not double-paint view A: the replay is
392
+ // scoped to the listeners the new render attached.
394
393
  const model = new Model<ModelState>({ count: 0 }, createMockComm());
395
394
  const listeners: Array<ReturnType<typeof vi.fn>> = [];
396
395
  const widgetDef = {
@@ -407,12 +406,52 @@ describe("WidgetBinding render listeners", () => {
407
406
  { el: document.createElement("div") },
408
407
  { signal: controller.signal },
409
408
  );
409
+ expect(listeners[0]).toHaveBeenCalledTimes(1);
410
+
411
+ await binding.createView(
412
+ { el: document.createElement("div") },
413
+ { signal: controller.signal },
414
+ );
415
+ // View B's listener hydrated once; view A's was left alone.
416
+ expect(listeners[1]).toHaveBeenCalledTimes(1);
417
+ expect(listeners[0]).toHaveBeenCalledTimes(1);
418
+ });
419
+
420
+ it("replays the any-change event to its listeners", async () => {
421
+ const model = new Model<ModelState>({ count: 1 }, createMockComm());
422
+ const onAnyChange = vi.fn();
423
+ const widgetDef = {
424
+ render: vi.fn(({ model }) => {
425
+ model.on("change", onAnyChange);
426
+ }),
427
+ };
428
+ const binding = await createBinding(widgetDef, model);
429
+ const controller = new AbortController();
430
+ await binding.createView(
431
+ { el: document.createElement("div") },
432
+ { signal: controller.signal },
433
+ );
434
+ expect(onAnyChange).toHaveBeenCalledTimes(1);
435
+ });
436
+
437
+ it("does not replay initialize listeners", async () => {
438
+ // The guarantee is per-view: initialize listeners existed before
439
+ // any view, and replaying at them would fire once per mount.
440
+ const model = new Model<ModelState>({ count: 1 }, createMockComm());
441
+ const initListener = vi.fn();
442
+ const widgetDef = {
443
+ initialize: vi.fn(({ model }) => {
444
+ model.on("change:count", initListener);
445
+ }),
446
+ render: vi.fn(),
447
+ };
448
+ const binding = await createBinding(widgetDef, model);
449
+ const controller = new AbortController();
410
450
  await binding.createView(
411
451
  { el: document.createElement("div") },
412
452
  { signal: controller.signal },
413
453
  );
414
- expect(listeners[0]).not.toHaveBeenCalled();
415
- expect(listeners[1]).not.toHaveBeenCalled();
454
+ expect(initListener).not.toHaveBeenCalled();
416
455
  });
417
456
 
418
457
  it("clears the element before rendering into it", async () => {
@@ -5,6 +5,15 @@ import type { ModelState } from "./types";
5
5
 
6
6
  type ModelEventCallback = Parameters<AnyModel<ModelState>["on"]>[1];
7
7
 
8
+ /**
9
+ * A listener registered through a model proxy (see the hydration
10
+ * replay in `WidgetBinding.createView`).
11
+ */
12
+ export interface ProxyRegistration {
13
+ event: string;
14
+ callback: ModelEventCallback;
15
+ }
16
+
8
17
  /**
9
18
  * Wrap a model so every `on()` call from inside `initialize` or `render`
10
19
  * is auto-tied to a lifetime `AbortSignal`. When the signal aborts,
@@ -18,11 +27,14 @@ type ModelEventCallback = Parameters<AnyModel<ModelState>["on"]>[1];
18
27
  * The proxy is purely a host-side ergonomic helper. The widget author
19
28
  * still writes `model.on("change:foo", handler)` exactly as before; the
20
29
  * cleanup signal is supplied transparently.
30
+ *
31
+ * `onRegister`, when given, observes each `on()` registration.
21
32
  */
22
33
  // oxlint-disable-next-line marimo/prefer-object-params -- concise internal helper used at protocol call sites
23
34
  export function modelProxy<T extends ModelState>(
24
35
  model: AnyModel<T>,
25
36
  signal: AbortSignal,
37
+ onRegister?: (registration: ProxyRegistration) => void,
26
38
  ): AnyModel<T> {
27
39
  return {
28
40
  get(key) {
@@ -43,6 +55,7 @@ export function modelProxy<T extends ModelState>(
43
55
  signal.addEventListener("abort", () => model.off(name, callback), {
44
56
  once: true,
45
57
  });
58
+ onRegister?.({ event: name, callback });
46
59
  },
47
60
  off(name?: string | null, callback?: ModelEventCallback | null): void {
48
61
  model.off(name ?? null, callback ?? null);
@@ -12,7 +12,7 @@ import { isTrustedVirtualFileUrl } from "@/plugins/core/trusted-url";
12
12
  import { Logger } from "@/utils/Logger";
13
13
  import type { Host } from "./host";
14
14
  import type { Model } from "./model";
15
- import { modelProxy } from "./model-proxy";
15
+ import { modelProxy, type ProxyRegistration } from "./model-proxy";
16
16
  import type { ModelState } from "./types";
17
17
 
18
18
  export const experimental: Experimental = {
@@ -266,8 +266,9 @@ export class WidgetBinding<T extends ModelState = ModelState> {
266
266
  * when the caller's `signal` fires or the binding is destroyed, and
267
267
  * listeners registered inside `render` auto-clear on abort.
268
268
  *
269
- * `render` reads current state via `model.get`; change listeners
270
- * observe only subsequent changes, matching Jupyter semantics.
269
+ * Hydration guarantee: listeners attached during `render` observe
270
+ * current model state exactly once, after `render` settles. Scoped
271
+ * to this view's listeners; re-firing at other views double-paints.
271
272
  */
272
273
  async createView(
273
274
  target: { el: HTMLElement },
@@ -304,8 +305,11 @@ export class WidgetBinding<T extends ModelState = ModelState> {
304
305
  // Each view gets a host scoped to its own signal so child views tear
305
306
  // down with this view.
306
307
  const host = this.#createHost(renderSignal);
308
+ const registrations: ProxyRegistration[] = [];
307
309
  const renderCleanup = await widget.render({
308
- model: modelProxy(this.#model, renderSignal),
310
+ model: modelProxy(this.#model, renderSignal, (registration) =>
311
+ registrations.push(registration),
312
+ ),
309
313
  el: target.el,
310
314
  experimental,
311
315
  signal: renderSignal,
@@ -329,6 +333,7 @@ export class WidgetBinding<T extends ModelState = ModelState> {
329
333
  once: true,
330
334
  });
331
335
  }
336
+ this.#replayState(registrations, renderSignal);
332
337
  }
333
338
 
334
339
  #trackCleanup(cleanup: Cleanup, reason: string): Promise<void> {
@@ -338,6 +343,31 @@ export class WidgetBinding<T extends ModelState = ModelState> {
338
343
  return task;
339
344
  }
340
345
 
346
+ /**
347
+ * The hydration guarantee documented on `createView`.
348
+ */
349
+ #replayState(
350
+ registrations: readonly ProxyRegistration[],
351
+ renderSignal: AbortSignal,
352
+ ): void {
353
+ const changePrefix = "change:";
354
+ for (const { event, callback } of registrations) {
355
+ if (renderSignal.aborted) {
356
+ return;
357
+ }
358
+ try {
359
+ if (event.startsWith(changePrefix)) {
360
+ const key = event.slice(changePrefix.length);
361
+ callback(this.#model.get(key));
362
+ } else if (event === "change") {
363
+ callback();
364
+ }
365
+ } catch (error) {
366
+ Logger.error("[WidgetBinding] Error replaying state", error);
367
+ }
368
+ }
369
+ }
370
+
341
371
  /**
342
372
  * Destroy this generation, running initialize/render cleanups and
343
373
  * clearing listeners registered through its model proxies.
@@ -12,13 +12,6 @@ export interface ImageComparisonData {
12
12
  height?: string;
13
13
  }
14
14
 
15
- // Truncate long sources (e.g. base64 data URLs) so error messages stay
16
- // readable.
17
- function truncateSrc(src: string): string {
18
- const MAX_LENGTH = 100;
19
- return src.length > MAX_LENGTH ? `${src.slice(0, MAX_LENGTH)}…` : src;
20
- }
21
-
22
15
  const ImageComparisonComponent: React.FC<ImageComparisonData> = ({
23
16
  beforeSrc,
24
17
  afterSrc,
@@ -27,61 +20,17 @@ const ImageComparisonComponent: React.FC<ImageComparisonData> = ({
27
20
  width,
28
21
  height,
29
22
  }) => {
30
- const [failedSrcs, setFailedSrcs] = React.useState<ReadonlySet<string>>(
31
- () => new Set(),
32
- );
33
-
34
- React.useEffect(() => {
35
- setFailedSrcs(new Set());
36
- }, [beforeSrc, afterSrc]);
37
-
38
- const handleError = React.useCallback((src: string) => {
39
- setFailedSrcs((prev) => new Set(prev).add(src));
40
- }, []);
41
-
42
23
  const containerStyle: React.CSSProperties = {
43
24
  width: width || "100%",
44
25
  height: height || (direction === "vertical" ? "400px" : "auto"),
45
26
  maxWidth: "100%",
46
- // The slider derives its height entirely from its (loaded) images, so a
47
- // broken/slow-loading source would otherwise collapse it to nothing and
48
- // render an empty output. Keep a minimum height so it stays visible.
49
- minHeight: "2rem",
50
27
  };
51
28
 
52
- // If an image fails to load, the slider collapses to nothing; surface a
53
- // visible error instead of silently rendering an empty output.
54
- if (failedSrcs.size > 0) {
55
- return (
56
- <div
57
- style={containerStyle}
58
- className="flex items-center justify-center rounded border border-destructive/40 bg-destructive/5 p-3 text-sm text-destructive"
59
- >
60
- <span>
61
- Failed to load {failedSrcs.size > 1 ? "images" : "image"}:{" "}
62
- {[...failedSrcs].map((src) => `"${truncateSrc(src)}"`).join(", ")}
63
- </span>
64
- </div>
65
- );
66
- }
67
-
68
29
  return (
69
30
  <div style={containerStyle}>
70
31
  <ImgComparisonSlider value={value} direction={direction}>
71
- <img
72
- slot="first"
73
- src={beforeSrc}
74
- alt="Before"
75
- width="100%"
76
- onError={() => handleError(beforeSrc)}
77
- />
78
- <img
79
- slot="second"
80
- src={afterSrc}
81
- alt="After"
82
- width="100%"
83
- onError={() => handleError(afterSrc)}
84
- />
32
+ <img slot="first" src={beforeSrc} alt="Before" width="100%" />
33
+ <img slot="second" src={afterSrc} alt="After" width="100%" />
85
34
  </ImgComparisonSlider>
86
35
  </div>
87
36
  );
@@ -1,11 +1,7 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
- import { afterEach, describe, expect, it, vi } from "vitest";
3
- import {
4
- type Data,
5
- MatplotlibRenderer,
6
- type MatplotlibState,
7
- visibleForTesting,
8
- } from "../matplotlib-renderer";
2
+ import { describe, expect, it } from "vitest";
3
+ import type { Data } from "../matplotlib-renderer";
4
+ import { visibleForTesting } from "../matplotlib-renderer";
9
5
 
10
6
  const {
11
7
  pixelToData,
@@ -189,67 +185,3 @@ describe("isInAxes", () => {
189
185
  expect(isInAxes({ x: 300, y: 351 }, LINEAR_AXES)).toBe(false);
190
186
  });
191
187
  });
192
-
193
- describe("MatplotlibRenderer", () => {
194
- afterEach(() => {
195
- vi.unstubAllGlobals();
196
- vi.restoreAllMocks();
197
- });
198
-
199
- it("restores a new selection when the chart changes", () => {
200
- vi.stubGlobal(
201
- "matchMedia",
202
- vi.fn(() => ({ addEventListener: vi.fn() })),
203
- );
204
- vi.stubGlobal(
205
- "requestAnimationFrame",
206
- vi.fn(() => 1),
207
- );
208
- vi.stubGlobal("cancelAnimationFrame", vi.fn());
209
- vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue(null);
210
-
211
- const value = {
212
- type: "box",
213
- has_selection: true,
214
- data: { x_min: 2, x_max: 4, y_min: 2, y_max: 4 },
215
- } as const;
216
- const state: MatplotlibState = {
217
- ...LINEAR_AXES,
218
- axesPixelBounds: [0, 0, 100, 100],
219
- yBounds: [0, 10],
220
- chartBase64: "first",
221
- width: 100,
222
- height: 100,
223
- selectionColor: "blue",
224
- selectionOpacity: 0.15,
225
- strokeWidth: 2,
226
- debounce: false,
227
- value: { has_selection: false },
228
- setValue: vi.fn(),
229
- };
230
- const container = document.createElement("div");
231
- const controller = new AbortController();
232
- const renderer = new MatplotlibRenderer(container, {
233
- state,
234
- signal: controller.signal,
235
- });
236
-
237
- // Cell reruns reuse the renderer, updating the chart and initial value together.
238
- renderer.update({ ...state, chartBase64: "second", value });
239
-
240
- const canvas = container.querySelector("canvas");
241
- expect(canvas).not.toBeNull();
242
- if (!canvas) {
243
- return;
244
- }
245
- vi.spyOn(canvas, "getBoundingClientRect").mockReturnValue(
246
- DOMRect.fromRect({ width: 100, height: 100 }),
247
- );
248
- canvas.dispatchEvent(
249
- new MouseEvent("pointermove", { clientX: 30, clientY: 70 }),
250
- );
251
-
252
- expect(canvas.style.cursor).toBe("move");
253
- controller.abort();
254
- });
255
- });
@@ -377,7 +377,6 @@ export class MatplotlibRenderer {
377
377
 
378
378
  if (state.chartBase64 !== this.#currentChartBase64) {
379
379
  this.#loadImage(state.chartBase64);
380
- this.#restoreSelection(state.value);
381
380
  return;
382
381
  }
383
382
 
@@ -2,7 +2,6 @@
2
2
 
3
3
  import { atom, useAtomValue } from "jotai";
4
4
  import { resolvedMarimoConfigAtom } from "@/core/config/config";
5
- import { KnownQueryParams } from "@/core/constants";
6
5
  import { isIslands } from "@/core/islands/utils";
7
6
  import { store } from "@/core/state/jotai";
8
7
 
@@ -11,16 +10,6 @@ export type ResolvedTheme = "light" | "dark";
11
10
 
12
11
  export const THEMES: Theme[] = ["light", "dark", "system"];
13
12
 
14
- function themeFromQueryParam(): Theme | undefined {
15
- if (typeof window === "undefined") {
16
- return undefined;
17
- }
18
- const value = new URLSearchParams(window.location.search).get(
19
- KnownQueryParams.theme,
20
- );
21
- return THEMES.includes(value as Theme) ? (value as Theme) : undefined;
22
- }
23
-
24
13
  const themeAtom = atom((get) => {
25
14
  // If it is islands, try a few ways to infer if it is dark mode.
26
15
  if (isIslands()) {
@@ -62,7 +51,7 @@ const themeAtom = atom((get) => {
62
51
  return "light";
63
52
  }
64
53
 
65
- return themeFromQueryParam() ?? get(resolvedMarimoConfigAtom).display.theme;
54
+ return get(resolvedMarimoConfigAtom).display.theme;
66
55
  });
67
56
 
68
57
  const prefersDarkModeAtom = atom(false);
@@ -133,7 +122,3 @@ export function useTheme(): { theme: ResolvedTheme } {
133
122
  const theme = useAtomValue(resolvedThemeAtom, { store });
134
123
  return { theme };
135
124
  }
136
-
137
- export const visibleForTesting = {
138
- themeFromQueryParam,
139
- };
@@ -1 +0,0 @@
1
- import{t as e}from"./worker-B3XPCb6y.js";var t=e(((e,t)=>{t.exports={}}));export default t();