@marimo-team/frontend 0.23.1-dev7 → 0.23.1-dev9

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 (78) hide show
  1. package/dist/assets/{CellStatus-1vKptZLk.js → CellStatus-zTcdYfqx.js} +1 -1
  2. package/dist/assets/{JsonOutput-BsAmBgmA.js → JsonOutput-BY31ccA7.js} +1 -1
  3. package/dist/assets/{MarimoErrorOutput-BrEeS6om.js → MarimoErrorOutput--Yd2Aw0J.js} +1 -1
  4. package/dist/assets/{RenderHTML-DvfCr2Ox.js → RenderHTML-CbuarQqA.js} +1 -1
  5. package/dist/assets/{add-cell-with-ai-Dt7oHdbm.js → add-cell-with-ai-_Y6SqxBB.js} +1 -1
  6. package/dist/assets/{add-connection-dialog-CspEHMf4.js → add-connection-dialog-CjvNOKgb.js} +1 -1
  7. package/dist/assets/{agent-panel-BnU7gQWx.js → agent-panel-C24uwabG.js} +1 -1
  8. package/dist/assets/{ai-model-dropdown-Xik-y-L5.js → ai-model-dropdown-Dyxi3_nW.js} +1 -1
  9. package/dist/assets/{app-config-button-CHBHFhjc.js → app-config-button-BT2Do4RJ.js} +1 -1
  10. package/dist/assets/{cell-editor-D_3l2zTD.js → cell-editor-zW0u82sK.js} +1 -1
  11. package/dist/assets/{cell-link-DJVe6_Zu.js → cell-link-CRkrHl-y.js} +1 -1
  12. package/dist/assets/{cells-S7rO4svP.js → cells-BqYYXi6G.js} +69 -69
  13. package/dist/assets/{chat-display-CfEsEqVW.js → chat-display-DsHMZa9F.js} +1 -1
  14. package/dist/assets/{chat-panel-qK2fGD8c.js → chat-panel-o9D3upnX.js} +1 -1
  15. package/dist/assets/{chat-ui-D0Zk2tGi.js → chat-ui-BYS03y86.js} +1 -1
  16. package/dist/assets/{column-preview-BZ6dVJcf.js → column-preview-Dwv5a_zE.js} +1 -1
  17. package/dist/assets/{command-palette-1FgTXBti.js → command-palette-BYbKGSF3.js} +1 -1
  18. package/dist/assets/{common-BmgcLq5w.js → common-DeoGL9rK.js} +1 -1
  19. package/dist/assets/{components-B_dPGsbP.js → components-CDgxb-5o.js} +1 -1
  20. package/dist/assets/{components-D2yZZbqm.js → components-DKHyHZBv.js} +1 -1
  21. package/dist/assets/{datasource-t6MwjjVj.js → datasource-COFRe84u.js} +1 -1
  22. package/dist/assets/{dependency-graph-panel-BadtKupA.js → dependency-graph-panel-BXSe6z1R.js} +1 -1
  23. package/dist/assets/{documentation-panel-BsLlmX7w.js → documentation-panel-CA2pWMgB.js} +1 -1
  24. package/dist/assets/{download-Do1WPYs4.js → download-5XbM3TL_.js} +1 -1
  25. package/dist/assets/edit-page-CMUN3ESy.js +9 -0
  26. package/dist/assets/{error-panel-CBVjdcTs.js → error-panel-CbqfK1HJ.js} +1 -1
  27. package/dist/assets/{file-explorer-panel-DYR37L0M.js → file-explorer-panel-CbS8z-JR.js} +1 -1
  28. package/dist/assets/{file-icons-Ce885dch.js → file-icons-Bj5YoM7H.js} +1 -1
  29. package/dist/assets/{floating-outline-x0sdO8LG.js → floating-outline-XObNWtN8.js} +1 -1
  30. package/dist/assets/{focus-DTtb8f52.js → focus-DzMo6UAI.js} +1 -1
  31. package/dist/assets/{form-D_Nha4Lp.js → form-DLyXacSF.js} +1 -1
  32. package/dist/assets/{home-page-CoJ_ZMWR.js → home-page-BUdd5uTz.js} +1 -1
  33. package/dist/assets/{hooks-Cx6iKOXA.js → hooks-kZJc1iBf.js} +1 -1
  34. package/dist/assets/{html-to-image-B2vXpMPW.js → html-to-image-DGqJ93hW.js} +1 -1
  35. package/dist/assets/{index-B_D5e64b.js → index-Bm25ctN7.js} +22 -22
  36. package/dist/assets/index-CKRn_SiB.css +2 -0
  37. package/dist/assets/{kiosk-mode-FcVQMZAH.js → kiosk-mode-DYHoqMaZ.js} +1 -1
  38. package/dist/assets/layout-tmN-U1zs.js +9 -0
  39. package/dist/assets/{logs-panel-Dsopo0A4.js → logs-panel-CRW4c2IL.js} +1 -1
  40. package/dist/assets/{markdown-renderer-Ds5PRrQP.js → markdown-renderer-DNANigO8.js} +1 -1
  41. package/dist/assets/{name-cell-input-CYTm4rHn.js → name-cell-input-3iKP6YTw.js} +1 -1
  42. package/dist/assets/{outline-panel-C6Gebwlt.js → outline-panel-VIqWcHj6.js} +1 -1
  43. package/dist/assets/{packages-panel-Cx5Im5-h.js → packages-panel-D_z4ylBE.js} +1 -1
  44. package/dist/assets/panels-CLfdzLPR.js +1 -0
  45. package/dist/assets/{process-output-DxNLeVL1.js → process-output-Q6wVr7a-.js} +1 -1
  46. package/dist/assets/{readonly-python-code-CB7U_Wc5.js → readonly-python-code-CI_b818F.js} +1 -1
  47. package/dist/assets/{run-page-CGoGL9nm.js → run-page-DPuH6QY4.js} +1 -1
  48. package/dist/assets/{scratchpad-panel-DR4mmtqX.js → scratchpad-panel-BsMm0GQP.js} +1 -1
  49. package/dist/assets/{session-panel-DGqZrbYK.js → session-panel-CTDzGShO.js} +1 -1
  50. package/dist/assets/{slides-component-BIXn0Nqk.js → slides-component-ncUJNz7U.js} +1 -1
  51. package/dist/assets/{snippets-panel-CU_AkTo5.js → snippets-panel-CWof0wHk.js} +1 -1
  52. package/dist/assets/{state-Di6_R3-d.js → state-BvnlMKdT.js} +1 -1
  53. package/dist/assets/{state-iGDxMYGl.js → state-DPomuurt.js} +1 -1
  54. package/dist/assets/{textarea-cV4DzEoq.js → textarea-CS2o3y4W.js} +1 -1
  55. package/dist/assets/{tracing-6MHdsIto.js → tracing-CPDDwzIA.js} +1 -1
  56. package/dist/assets/{tracing-panel-Cwuf0kYN.js → tracing-panel-Ku1LapXJ.js} +2 -2
  57. package/dist/assets/{useAddCell-DDDgUZhC.js → useAddCell-B6yUY_RG.js} +1 -1
  58. package/dist/assets/{useCellActionButton-BceYv-6H.js → useCellActionButton-SxeK4dmW.js} +1 -1
  59. package/dist/assets/{useDeleteCell-CwBNr3-p.js → useDeleteCell-DHUjJQJx.js} +1 -1
  60. package/dist/assets/{useDependencyPanelTab-Bjv6Z79M.js → useDependencyPanelTab-CflgayoH.js} +1 -1
  61. package/dist/assets/{useNotebookActions-0DS32qpY.js → useNotebookActions-DHBEqrc_.js} +1 -1
  62. package/dist/assets/{useRunCells-Bf82xWy5.js → useRunCells-DFYAOTWd.js} +1 -1
  63. package/dist/assets/{useSplitCell-WZ71D3bV.js → useSplitCell-Bh-NZsBl.js} +1 -1
  64. package/dist/index.html +23 -23
  65. package/package.json +1 -1
  66. package/src/components/editor/renderers/slides-layout/slides-layout.tsx +50 -44
  67. package/src/components/slides/__tests__/minimap.test.ts +402 -0
  68. package/src/components/slides/minimap.tsx +534 -0
  69. package/src/components/slides/slide.tsx +29 -0
  70. package/src/components/slides/slides-component.tsx +16 -1
  71. package/src/core/cells/__tests__/cells.test.ts +105 -1
  72. package/src/core/cells/cells.ts +43 -0
  73. package/src/core/cells/document-changes.ts +2 -1
  74. package/src/plugins/impl/matplotlib/matplotlib-renderer.ts +38 -14
  75. package/dist/assets/edit-page-CSyxrzTp.js +0 -13
  76. package/dist/assets/index-qO0a4zuT.css +0 -2
  77. package/dist/assets/layout-DW9T7Upe.js +0 -5
  78. package/dist/assets/panels-CDCHQBRn.js +0 -1
@@ -0,0 +1,402 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { renderHook, act } from "@testing-library/react";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+ import type { CellId } from "@/core/cells/ids";
6
+ import type { DragMoveEvent } from "@dnd-kit/core";
7
+ import { MultiColumn } from "@/utils/id-tree";
8
+ import { exportedForTesting } from "../minimap";
9
+
10
+ const { useVisibleCellIds, projectDropTarget, resolveDropTarget } =
11
+ exportedForTesting;
12
+
13
+ let intersectionCallback: IntersectionObserverCallback;
14
+ let mutationCallback: MutationCallback;
15
+ let observeSpy: ReturnType<typeof vi.fn>;
16
+ let intersectionDisconnectSpy: ReturnType<typeof vi.fn>;
17
+ let mutationDisconnectSpy: ReturnType<typeof vi.fn>;
18
+ let mutationObserveSpy: ReturnType<typeof vi.fn>;
19
+
20
+ function createContainer(...cellIds: string[]): HTMLDivElement {
21
+ const container = document.createElement("div");
22
+ for (const id of cellIds) {
23
+ const child = document.createElement("button");
24
+ child.dataset.cellId = id;
25
+ container.appendChild(child);
26
+ }
27
+ return container;
28
+ }
29
+
30
+ function fakeEntry(
31
+ target: Element,
32
+ isIntersecting: boolean,
33
+ ): IntersectionObserverEntry {
34
+ return { target, isIntersecting } as unknown as IntersectionObserverEntry;
35
+ }
36
+
37
+ describe("useVisibleCellIds", () => {
38
+ beforeEach(() => {
39
+ observeSpy = vi.fn();
40
+ intersectionDisconnectSpy = vi.fn();
41
+ mutationDisconnectSpy = vi.fn();
42
+ mutationObserveSpy = vi.fn();
43
+
44
+ global.IntersectionObserver = class MockIntersectionObserver {
45
+ constructor(
46
+ callback: IntersectionObserverCallback,
47
+ _options?: IntersectionObserverInit,
48
+ ) {
49
+ intersectionCallback = callback;
50
+ }
51
+ observe = observeSpy;
52
+ unobserve = vi.fn();
53
+ disconnect = intersectionDisconnectSpy;
54
+ root = null;
55
+ rootMargin = "";
56
+ thresholds = [0];
57
+ takeRecords = vi.fn(() => []);
58
+ } as unknown as typeof IntersectionObserver;
59
+
60
+ global.MutationObserver = class MockMutationObserver {
61
+ constructor(callback: MutationCallback) {
62
+ mutationCallback = callback;
63
+ }
64
+ observe = mutationObserveSpy;
65
+ disconnect = mutationDisconnectSpy;
66
+ takeRecords = vi.fn(() => []);
67
+ } as unknown as typeof MutationObserver;
68
+ });
69
+
70
+ afterEach(() => {
71
+ vi.restoreAllMocks();
72
+ });
73
+
74
+ it("returns an empty set initially", () => {
75
+ const container = createContainer("cell-1");
76
+ const ref = { current: container };
77
+
78
+ const { result } = renderHook(() => useVisibleCellIds(ref));
79
+
80
+ expect(result.current.size).toBe(0);
81
+ });
82
+
83
+ it("observes all [data-cell-id] elements on mount", () => {
84
+ const container = createContainer("a", "b", "c");
85
+ const ref = { current: container };
86
+
87
+ renderHook(() => useVisibleCellIds(ref));
88
+
89
+ expect(observeSpy).toHaveBeenCalledTimes(3);
90
+ });
91
+
92
+ it("does not observe when container ref is null", () => {
93
+ const ref = { current: null };
94
+
95
+ const { result } = renderHook(() => useVisibleCellIds(ref));
96
+
97
+ expect(observeSpy).not.toHaveBeenCalled();
98
+ expect(result.current.size).toBe(0);
99
+ });
100
+
101
+ it("adds cell ids when entries become intersecting", () => {
102
+ const container = createContainer("cell-1", "cell-2");
103
+ const ref = { current: container };
104
+ const children = container.querySelectorAll("[data-cell-id]");
105
+
106
+ const { result } = renderHook(() => useVisibleCellIds(ref));
107
+
108
+ act(() => {
109
+ intersectionCallback(
110
+ [fakeEntry(children[0], true)],
111
+ {} as IntersectionObserver,
112
+ );
113
+ });
114
+
115
+ expect(result.current.has("cell-1" as CellId)).toBe(true);
116
+ expect(result.current.has("cell-2" as CellId)).toBe(false);
117
+ });
118
+
119
+ it("removes cell ids when entries stop intersecting", () => {
120
+ const container = createContainer("cell-1");
121
+ const ref = { current: container };
122
+ const children = container.querySelectorAll("[data-cell-id]");
123
+
124
+ const { result } = renderHook(() => useVisibleCellIds(ref));
125
+
126
+ act(() => {
127
+ intersectionCallback(
128
+ [fakeEntry(children[0], true)],
129
+ {} as IntersectionObserver,
130
+ );
131
+ });
132
+ expect(result.current.has("cell-1" as CellId)).toBe(true);
133
+
134
+ act(() => {
135
+ intersectionCallback(
136
+ [fakeEntry(children[0], false)],
137
+ {} as IntersectionObserver,
138
+ );
139
+ });
140
+ expect(result.current.has("cell-1" as CellId)).toBe(false);
141
+ });
142
+
143
+ it("preserves set identity when nothing changed", () => {
144
+ const container = createContainer("cell-1");
145
+ const ref = { current: container };
146
+ const children = container.querySelectorAll("[data-cell-id]");
147
+
148
+ const { result } = renderHook(() => useVisibleCellIds(ref));
149
+
150
+ act(() => {
151
+ intersectionCallback(
152
+ [fakeEntry(children[0], true)],
153
+ {} as IntersectionObserver,
154
+ );
155
+ });
156
+ const firstSet = result.current;
157
+
158
+ act(() => {
159
+ intersectionCallback(
160
+ [fakeEntry(children[0], true)],
161
+ {} as IntersectionObserver,
162
+ );
163
+ });
164
+ expect(result.current).toBe(firstSet);
165
+ });
166
+
167
+ it("handles batch intersection updates", () => {
168
+ const container = createContainer("a", "b", "c");
169
+ const ref = { current: container };
170
+ const children = container.querySelectorAll("[data-cell-id]");
171
+
172
+ const { result } = renderHook(() => useVisibleCellIds(ref));
173
+
174
+ act(() => {
175
+ intersectionCallback(
176
+ [
177
+ fakeEntry(children[0], true),
178
+ fakeEntry(children[1], true),
179
+ fakeEntry(children[2], false),
180
+ ],
181
+ {} as IntersectionObserver,
182
+ );
183
+ });
184
+
185
+ expect(result.current).toEqual(new Set(["a", "b"]));
186
+ });
187
+
188
+ it("ignores entries without data-cell-id", () => {
189
+ const container = createContainer("cell-1");
190
+ const orphan = document.createElement("div");
191
+ container.appendChild(orphan);
192
+ const ref = { current: container };
193
+
194
+ const { result } = renderHook(() => useVisibleCellIds(ref));
195
+
196
+ act(() => {
197
+ intersectionCallback(
198
+ [fakeEntry(orphan, true)],
199
+ {} as IntersectionObserver,
200
+ );
201
+ });
202
+
203
+ expect(result.current.size).toBe(0);
204
+ });
205
+
206
+ it("re-observes elements when MutationObserver fires", () => {
207
+ const container = createContainer("cell-1");
208
+ const ref = { current: container };
209
+
210
+ renderHook(() => useVisibleCellIds(ref));
211
+ expect(observeSpy).toHaveBeenCalledTimes(1);
212
+
213
+ const newChild = document.createElement("button");
214
+ newChild.dataset.cellId = "cell-2";
215
+ container.appendChild(newChild);
216
+
217
+ act(() => {
218
+ mutationCallback([], {} as MutationObserver);
219
+ });
220
+
221
+ // 1 from initial + 2 from re-observe (observe is idempotent on existing)
222
+ expect(observeSpy).toHaveBeenCalledTimes(3);
223
+ });
224
+
225
+ it("disconnects both observers on unmount", () => {
226
+ const container = createContainer("cell-1");
227
+ const ref = { current: container };
228
+
229
+ const { unmount } = renderHook(() => useVisibleCellIds(ref));
230
+
231
+ // 1 disconnect from the initial observeAll() call
232
+ expect(intersectionDisconnectSpy).toHaveBeenCalledTimes(1);
233
+ expect(mutationDisconnectSpy).not.toHaveBeenCalled();
234
+
235
+ unmount();
236
+
237
+ // +1 disconnect from cleanup
238
+ expect(intersectionDisconnectSpy).toHaveBeenCalledTimes(2);
239
+ expect(mutationDisconnectSpy).toHaveBeenCalledTimes(1);
240
+ });
241
+ });
242
+
243
+ function rect(top: number, height: number) {
244
+ return { top, height, left: 0, right: 0, bottom: 0, width: 0 };
245
+ }
246
+
247
+ type Rect = ReturnType<typeof rect>;
248
+
249
+ function fakeDragEvent({
250
+ activeId = "a" as string | number,
251
+ overId = "b" as string | number | null,
252
+ translated = rect(0, 40) as Rect | null,
253
+ initial = undefined as Rect | null | undefined,
254
+ overRect = rect(0, 40),
255
+ } = {}): DragMoveEvent {
256
+ return {
257
+ active: {
258
+ id: activeId,
259
+ rect: { current: { translated, initial: initial ?? translated } },
260
+ },
261
+ over: overId === null ? null : { id: overId, rect: overRect },
262
+ } as unknown as DragMoveEvent;
263
+ }
264
+
265
+ describe("projectDropTarget", () => {
266
+ it("returns null when over is null", () => {
267
+ expect(projectDropTarget(fakeDragEvent({ overId: null }))).toBeNull();
268
+ });
269
+
270
+ it("returns null when active id is not a string", () => {
271
+ expect(projectDropTarget(fakeDragEvent({ activeId: 42 }))).toBeNull();
272
+ });
273
+
274
+ it("returns null when over id is not a string", () => {
275
+ expect(projectDropTarget(fakeDragEvent({ overId: 42 }))).toBeNull();
276
+ });
277
+
278
+ it("returns null when active and over are the same cell", () => {
279
+ expect(
280
+ projectDropTarget(fakeDragEvent({ activeId: "x", overId: "x" })),
281
+ ).toBeNull();
282
+ });
283
+
284
+ it("returns null when no active rect is available", () => {
285
+ expect(
286
+ projectDropTarget(fakeDragEvent({ translated: null, initial: null })),
287
+ ).toBeNull();
288
+ });
289
+
290
+ it("falls back to initial rect when translated is null", () => {
291
+ expect(
292
+ projectDropTarget(
293
+ fakeDragEvent({
294
+ translated: null,
295
+ initial: rect(0, 40),
296
+ overRect: rect(100, 40),
297
+ }),
298
+ ),
299
+ ).toEqual({ overId: "b", position: "before" });
300
+ });
301
+
302
+ it("prefers translated rect over initial rect", () => {
303
+ expect(
304
+ projectDropTarget(
305
+ fakeDragEvent({
306
+ translated: rect(200, 40),
307
+ initial: rect(0, 40),
308
+ overRect: rect(100, 40),
309
+ }),
310
+ ),
311
+ ).toEqual({ overId: "b", position: "after" });
312
+ });
313
+ });
314
+
315
+ describe("resolveDropTarget", () => {
316
+ const A = "a" as CellId;
317
+ const B = "b" as CellId;
318
+ const C = "c" as CellId;
319
+ const D = "d" as CellId;
320
+
321
+ function makeSingleColumnIds(...ids: CellId[]) {
322
+ return MultiColumn.from([ids]);
323
+ }
324
+
325
+ it("returns null when activeId matches target overId", () => {
326
+ const cellIds = makeSingleColumnIds(A, B);
327
+ expect(
328
+ resolveDropTarget({
329
+ cellIds,
330
+ activeId: A,
331
+ target: { overId: A, position: "after" },
332
+ }),
333
+ ).toBeNull();
334
+ });
335
+
336
+ it("returns null for multi-column layouts", () => {
337
+ const cellIds = MultiColumn.from([[A, B], [C]]);
338
+ expect(
339
+ resolveDropTarget({
340
+ cellIds,
341
+ activeId: A,
342
+ target: { overId: B, position: "after" },
343
+ }),
344
+ ).toBeNull();
345
+ });
346
+
347
+ it('resolves "before" to the over index', () => {
348
+ const cellIds = makeSingleColumnIds(A, B, C);
349
+ const result = resolveDropTarget({
350
+ cellIds,
351
+ activeId: A,
352
+ target: { overId: C, position: "before" },
353
+ });
354
+ expect(result).toEqual({
355
+ cellId: A,
356
+ columnId: cellIds.atOrThrow(0).id,
357
+ index: 2,
358
+ });
359
+ });
360
+
361
+ it('resolves "after" to one past the over index', () => {
362
+ const cellIds = makeSingleColumnIds(A, B, C);
363
+ const result = resolveDropTarget({
364
+ cellIds,
365
+ activeId: C,
366
+ target: { overId: A, position: "after" },
367
+ });
368
+ expect(result).toEqual({
369
+ cellId: C,
370
+ columnId: cellIds.atOrThrow(0).id,
371
+ index: 1,
372
+ });
373
+ });
374
+
375
+ it('resolves "before" on first item to index 0', () => {
376
+ const cellIds = makeSingleColumnIds(A, B, C);
377
+ const result = resolveDropTarget({
378
+ cellIds,
379
+ activeId: C,
380
+ target: { overId: A, position: "before" },
381
+ });
382
+ expect(result).toEqual({
383
+ cellId: C,
384
+ columnId: cellIds.atOrThrow(0).id,
385
+ index: 0,
386
+ });
387
+ });
388
+
389
+ it('resolves "after" on last item to one past the end', () => {
390
+ const cellIds = makeSingleColumnIds(A, B, C, D);
391
+ const result = resolveDropTarget({
392
+ cellIds,
393
+ activeId: A,
394
+ target: { overId: D, position: "after" },
395
+ });
396
+ expect(result).toEqual({
397
+ cellId: A,
398
+ columnId: cellIds.atOrThrow(0).id,
399
+ index: 4,
400
+ });
401
+ });
402
+ });