@marimo-team/islands 0.23.3-dev9 → 0.23.4-dev0

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 (42) hide show
  1. package/dist/{chat-ui-BLFhPclV.js → chat-ui-DEd_Ndal.js} +82 -82
  2. package/dist/{html-to-image-XYwXqg2E.js → html-to-image-DBosi5GK.js} +2240 -2214
  3. package/dist/main.js +2627 -2746
  4. package/dist/{process-output-BDVjDpbu.js → process-output-k-4WHpxz.js} +1 -1
  5. package/dist/{reveal-component-CrnLosc4.js → reveal-component-CFuofbBD.js} +827 -561
  6. package/dist/{slide-Dl7Rf496.js → slide-form-DgMI37ES.js} +1729 -894
  7. package/dist/style.css +1 -1
  8. package/package.json +1 -1
  9. package/src/components/editor/file-tree/renderers.tsx +1 -1
  10. package/src/components/editor/output/JsonOutput.tsx +187 -4
  11. package/src/components/editor/output/__tests__/JsonOutput-mimetype.test.tsx +80 -0
  12. package/src/components/editor/output/__tests__/json-output.test.ts +185 -2
  13. package/src/components/editor/renderers/slides-layout/__tests__/compute-slide-cells.test.ts +150 -0
  14. package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +298 -0
  15. package/src/components/editor/renderers/slides-layout/compute-slide-cells.ts +50 -0
  16. package/src/components/editor/renderers/slides-layout/plugin.tsx +54 -9
  17. package/src/components/editor/renderers/slides-layout/slides-layout.tsx +30 -12
  18. package/src/components/editor/renderers/slides-layout/types.ts +31 -3
  19. package/src/components/editor/renderers/types.ts +2 -0
  20. package/src/components/slides/__tests__/compose-slides.test.ts +433 -0
  21. package/src/components/slides/compose-slides.ts +337 -0
  22. package/src/components/slides/minimap.tsx +133 -12
  23. package/src/components/slides/reveal-component.tsx +337 -74
  24. package/src/components/slides/reveal-slides.css +33 -1
  25. package/src/components/slides/slide-form.tsx +347 -0
  26. package/src/components/ui/radio-group.tsx +5 -3
  27. package/src/core/cells/types.ts +2 -0
  28. package/src/core/islands/__tests__/bridge.test.ts +116 -5
  29. package/src/core/islands/bridge.ts +5 -1
  30. package/src/core/layout/layout.ts +6 -2
  31. package/src/core/static/__tests__/export-context.test.ts +122 -0
  32. package/src/core/static/__tests__/static-state.test.ts +80 -0
  33. package/src/core/static/export-context.ts +84 -0
  34. package/src/core/static/static-state.ts +44 -6
  35. package/src/plugins/core/RenderHTML.tsx +23 -2
  36. package/src/plugins/core/__test__/RenderHTML.test.ts +86 -1
  37. package/src/plugins/core/__test__/trusted-url.test.ts +130 -18
  38. package/src/plugins/core/sanitize.ts +11 -5
  39. package/src/plugins/core/trusted-url.ts +32 -10
  40. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +29 -1
  41. package/src/plugins/impl/mpl-interactive/__tests__/MplInteractivePlugin.test.tsx +34 -0
  42. package/src/plugins/impl/panel/__tests__/PanelPlugin.test.ts +35 -2
@@ -1,13 +1,41 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  /* oxlint-disable typescript/no-empty-object-type */
3
3
 
4
+ import type { CellId } from "@/core/cells/ids";
5
+
4
6
  /**
5
7
  * The serialized form of a slides layout.
6
8
  * This must be backwards-compatible as it is stored on the user's disk.
7
9
  */
8
10
  // oxlint-disable-next-line typescript/consistent-type-definitions
9
- export type SerializedSlidesLayout = {};
11
+ export type SerializedSlidesLayout = {
12
+ // Both fields are optional so files saved before these existed (e.g. the
13
+ // bare `{}` emitted by earlier marimo versions) still deserialize cleanly.
14
+ deck?: DeckConfig;
15
+ cells?: SlideConfig[];
16
+ };
17
+
18
+ export interface SlidesLayout extends Omit<
19
+ SerializedSlidesLayout,
20
+ "cells" | "deck"
21
+ > {
22
+ // We map the cells to their IDs so that we can track them as they move around.
23
+ cells: Map<CellId, SlideConfig>;
24
+ deck: DeckConfig;
25
+ }
26
+
27
+ export type SlideType = "slide" | "sub-slide" | "fragment" | "skip";
28
+ export interface SlideConfig {
29
+ type?: SlideType;
30
+ }
10
31
 
11
- export interface SlidesLayout extends SerializedSlidesLayout {
12
- // No additional properties for now
32
+ export type DeckTransition =
33
+ | "none"
34
+ | "fade"
35
+ | "slide"
36
+ | "convex"
37
+ | "concave"
38
+ | "zoom";
39
+ export interface DeckConfig {
40
+ transition?: DeckTransition;
13
41
  }
@@ -66,7 +66,9 @@ export interface ICellRendererPlugin<S, L> {
66
66
  */
67
67
  validator: ZodType<S>;
68
68
 
69
+ // Take a serialized layout and a list of cells, and return a layout object.
69
70
  deserializeLayout: (layout: S, cells: CellData[]) => L;
71
+ // Take the in-memory layout object and a list of cells, and return a serialized layout.
70
72
  serializeLayout: (layout: L, cells: CellData[]) => S;
71
73
 
72
74
  Component: React.FC<ICellRendererProps<L>>;
@@ -0,0 +1,433 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { describe, it, expect } from "vitest";
4
+ import {
5
+ buildSlideIndices,
6
+ composeSlides,
7
+ computeDeckNavigation,
8
+ resolveDeckNavigationTarget,
9
+ resolveActiveCellIndex,
10
+ } from "../compose-slides";
11
+ import type { SlideType } from "@/components/editor/renderers/slides-layout/types";
12
+
13
+ interface Cell {
14
+ id: string;
15
+ /**
16
+ * Optional for ergonomics; mirrors the on-disk shape where a missing
17
+ * `type` means "use the default slide type". The `compose` helper below
18
+ * normalizes this to `"slide"` before passing through.
19
+ */
20
+ type?: SlideType;
21
+ }
22
+
23
+ const compose = (cells: Cell[]) =>
24
+ composeSlides({
25
+ cells,
26
+ getType: (c) => c.type ?? "slide",
27
+ });
28
+
29
+ /**
30
+ * Collapse the tree to a readable shape so failures produce tiny, obvious
31
+ * diffs:
32
+ * stacks -> subslides -> blocks -> { f: isFragment, ids: [...] }
33
+ */
34
+ const shape = (cells: Cell[]) =>
35
+ compose(cells).stacks.map((s) =>
36
+ s.subslides.map((sub) =>
37
+ sub.blocks.map((b) => ({
38
+ f: b.isFragment,
39
+ ids: b.cells.map((x) => x.id),
40
+ })),
41
+ ),
42
+ );
43
+
44
+ describe("composeSlides", () => {
45
+ it("returns an empty composition for empty input", () => {
46
+ expect(compose([])).toEqual({ stacks: [] });
47
+ });
48
+
49
+ it("treats each 'slide' cell as its own stack", () => {
50
+ expect(
51
+ shape([
52
+ { id: "a", type: "slide" },
53
+ { id: "b", type: "slide" },
54
+ { id: "c", type: "slide" },
55
+ ]),
56
+ ).toEqual([
57
+ [[{ f: false, ids: ["a"] }]],
58
+ [[{ f: false, ids: ["b"] }]],
59
+ [[{ f: false, ids: ["c"] }]],
60
+ ]);
61
+ });
62
+
63
+ it("nests 'sub-slide' cells under the current slide (vertical stack)", () => {
64
+ expect(
65
+ shape([
66
+ { id: "a", type: "slide" },
67
+ { id: "b", type: "sub-slide" },
68
+ { id: "c", type: "sub-slide" },
69
+ { id: "d", type: "slide" },
70
+ ]),
71
+ ).toEqual([
72
+ [
73
+ [{ f: false, ids: ["a"] }],
74
+ [{ f: false, ids: ["b"] }],
75
+ [{ f: false, ids: ["c"] }],
76
+ ],
77
+ [[{ f: false, ids: ["d"] }]],
78
+ ]);
79
+ });
80
+
81
+ it("wraps fragments in their own block on the current subslide", () => {
82
+ expect(
83
+ shape([
84
+ { id: "a", type: "slide" },
85
+ { id: "b", type: "fragment" },
86
+ { id: "c", type: "fragment" },
87
+ ]),
88
+ ).toEqual([
89
+ [
90
+ [
91
+ { f: false, ids: ["a"] },
92
+ { f: true, ids: ["b"] },
93
+ { f: true, ids: ["c"] },
94
+ ],
95
+ ],
96
+ ]);
97
+ });
98
+
99
+ it("treats cells with no type as their own new slide (the default)", () => {
100
+ expect(
101
+ shape([
102
+ { id: "a", type: "slide" },
103
+ { id: "b" },
104
+ { id: "c", type: "fragment" },
105
+ { id: "d" },
106
+ ]),
107
+ ).toEqual([
108
+ [[{ f: false, ids: ["a"] }]],
109
+ [
110
+ [
111
+ { f: false, ids: ["b"] },
112
+ { f: true, ids: ["c"] },
113
+ ],
114
+ ],
115
+ [[{ f: false, ids: ["d"] }]],
116
+ ]);
117
+ });
118
+
119
+ it("creates an implicit initial subslide when the first cell is a fragment", () => {
120
+ expect(shape([{ id: "a", type: "fragment" }])).toEqual([
121
+ [[{ f: true, ids: ["a"] }]],
122
+ ]);
123
+ });
124
+
125
+ it("creates an implicit initial stack when the first cell is a sub-slide", () => {
126
+ expect(
127
+ shape([
128
+ { id: "a", type: "sub-slide" },
129
+ { id: "b", type: "sub-slide" },
130
+ ]),
131
+ ).toEqual([[[{ f: false, ids: ["a"] }], [{ f: false, ids: ["b"] }]]]);
132
+ });
133
+
134
+ it("drops 'skip' cells from the deck", () => {
135
+ expect(
136
+ shape([
137
+ { id: "a", type: "slide" },
138
+ { id: "b", type: "skip" },
139
+ { id: "c", type: "fragment" },
140
+ ]),
141
+ ).toEqual([
142
+ [
143
+ [
144
+ { f: false, ids: ["a"] },
145
+ { f: true, ids: ["c"] },
146
+ ],
147
+ ],
148
+ ]);
149
+ });
150
+
151
+ it("skip at the very start does not create an empty leading stack", () => {
152
+ expect(
153
+ shape([
154
+ { id: "a", type: "skip" },
155
+ { id: "b", type: "slide" },
156
+ ]),
157
+ ).toEqual([[[{ f: false, ids: ["b"] }]]]);
158
+ });
159
+
160
+ it("opens a fresh fragment block for each consecutive fragment cell", () => {
161
+ expect(
162
+ shape([
163
+ { id: "a", type: "slide" },
164
+ { id: "b", type: "fragment" },
165
+ { id: "c", type: "fragment" },
166
+ { id: "d", type: "fragment" },
167
+ ]),
168
+ ).toEqual([
169
+ [
170
+ [
171
+ { f: false, ids: ["a"] },
172
+ { f: true, ids: ["b"] },
173
+ { f: true, ids: ["c"] },
174
+ { f: true, ids: ["d"] },
175
+ ],
176
+ ],
177
+ ]);
178
+ });
179
+
180
+ it("resets fragment context when a new subslide opens", () => {
181
+ expect(
182
+ shape([
183
+ { id: "a", type: "slide" },
184
+ { id: "b", type: "fragment" },
185
+ { id: "c", type: "sub-slide" },
186
+ { id: "d", type: "fragment" },
187
+ ]),
188
+ ).toEqual([
189
+ [
190
+ [
191
+ { f: false, ids: ["a"] },
192
+ { f: true, ids: ["b"] },
193
+ ],
194
+ [
195
+ { f: false, ids: ["c"] },
196
+ { f: true, ids: ["d"] },
197
+ ],
198
+ ],
199
+ ]);
200
+ });
201
+
202
+ it("handles a realistic mixed sequence", () => {
203
+ expect(
204
+ shape([
205
+ { id: "title", type: "slide" },
206
+ { id: "intro", type: "fragment" },
207
+ { id: "deep", type: "sub-slide" },
208
+ { id: "deep-body", type: "fragment" },
209
+ { id: "debug", type: "skip" },
210
+ { id: "outro", type: "slide" },
211
+ ]),
212
+ ).toEqual([
213
+ [
214
+ [
215
+ { f: false, ids: ["title"] },
216
+ { f: true, ids: ["intro"] },
217
+ ],
218
+ [
219
+ { f: false, ids: ["deep"] },
220
+ { f: true, ids: ["deep-body"] },
221
+ ],
222
+ ],
223
+ [[{ f: false, ids: ["outro"] }]],
224
+ ]);
225
+ });
226
+
227
+ it("is generic over cell shape (preserves object identity)", () => {
228
+ const a = { id: "a", type: "slide" as const, extra: 42 };
229
+ const b = { id: "b", type: "fragment" as const, extra: 7 };
230
+ const result = composeSlides({ cells: [a, b], getType: (c) => c.type });
231
+ expect(result.stacks[0].subslides[0].blocks[0].cells[0]).toBe(a);
232
+ expect(result.stacks[0].subslides[0].blocks[1].cells[0]).toBe(b);
233
+ });
234
+ });
235
+
236
+ describe("buildSlideIndices", () => {
237
+ const build = (cells: Cell[]) => {
238
+ const composition = composeSlides({
239
+ cells,
240
+ getType: (c) => c.type ?? "slide",
241
+ });
242
+ return buildSlideIndices({ composition, cells, getId: (c) => c.id });
243
+ };
244
+
245
+ it("maps each cell to its {h, v, f} location", () => {
246
+ const cells: Cell[] = [
247
+ { id: "a", type: "slide" },
248
+ { id: "b", type: "fragment" },
249
+ { id: "c", type: "sub-slide" },
250
+ { id: "d", type: "slide" },
251
+ { id: "e", type: "fragment" },
252
+ { id: "f", type: "fragment" },
253
+ ];
254
+ const { cellToTarget } = build(cells);
255
+ expect(cellToTarget.get("a")).toEqual({ h: 0, v: 0, f: -1 });
256
+ expect(cellToTarget.get("b")).toEqual({ h: 0, v: 0, f: 0 });
257
+ expect(cellToTarget.get("c")).toEqual({ h: 0, v: 1, f: -1 });
258
+ expect(cellToTarget.get("d")).toEqual({ h: 1, v: 0, f: -1 });
259
+ expect(cellToTarget.get("e")).toEqual({ h: 1, v: 0, f: 0 });
260
+ expect(cellToTarget.get("f")).toEqual({ h: 1, v: 0, f: 1 });
261
+ });
262
+
263
+ it("maps {h, v, f} back to the flat index of the last visible cell", () => {
264
+ const cells: Cell[] = [
265
+ { id: "a", type: "slide" },
266
+ { id: "b", type: "fragment" },
267
+ { id: "c", type: "fragment" },
268
+ ];
269
+ const { targetToCellIndex } = build(cells);
270
+ // Before any fragment is revealed, the active cell is the last pre-fragment cell.
271
+ expect(targetToCellIndex.get("0,0,-1")).toBe(0); // "a"
272
+ // After fragment 0 is shown, active advances to "b".
273
+ expect(targetToCellIndex.get("0,0,0")).toBe(1); // "b"
274
+ // After fragment 1 is shown, active advances to "c".
275
+ expect(targetToCellIndex.get("0,0,1")).toBe(2); // "c"
276
+ });
277
+
278
+ it("populates f=-1 with the first cell when the subslide starts with a fragment", () => {
279
+ const cells: Cell[] = [{ id: "a", type: "fragment" }];
280
+ const { targetToCellIndex } = build(cells);
281
+ expect(targetToCellIndex.get("0,0,-1")).toBe(0);
282
+ expect(targetToCellIndex.get("0,0,0")).toBe(0);
283
+ });
284
+
285
+ it("drops skipped cells from the index entirely", () => {
286
+ const cells: Cell[] = [
287
+ { id: "a", type: "slide" },
288
+ { id: "b", type: "skip" },
289
+ ];
290
+ const { cellToTarget } = build(cells);
291
+ expect(cellToTarget.has("a")).toBe(true);
292
+ expect(cellToTarget.has("b")).toBe(false);
293
+ });
294
+ });
295
+
296
+ describe("resolveActiveCellIndex", () => {
297
+ const map = new Map<string, number>([
298
+ ["0,0,-1", 0],
299
+ ["0,0,0", 1],
300
+ ["0,0,1", 2],
301
+ ["1,0,-1", 3],
302
+ ]);
303
+
304
+ it("returns the exact match when one exists", () => {
305
+ expect(resolveActiveCellIndex(map, { h: 0, v: 0, f: 1 })).toBe(2);
306
+ });
307
+
308
+ it("falls back to f=-1 when there is no fragment-specific entry", () => {
309
+ // reveal may report f=0 for slides without any fragments
310
+ expect(resolveActiveCellIndex(map, { h: 1, v: 0, f: 0 })).toBe(3);
311
+ });
312
+
313
+ it("returns undefined for unknown stacks", () => {
314
+ expect(resolveActiveCellIndex(map, { h: 9, v: 0, f: 0 })).toBeUndefined();
315
+ });
316
+ });
317
+
318
+ describe("resolveDeckNavigationTarget", () => {
319
+ const resolve = (cells: Cell[], activeIndex: number | undefined) => {
320
+ const composition = compose(cells);
321
+ const { cellToTarget } = buildSlideIndices({
322
+ composition,
323
+ cells,
324
+ getId: (cell) => cell.id,
325
+ });
326
+ return resolveDeckNavigationTarget({
327
+ activeIndex,
328
+ cells,
329
+ cellToTarget,
330
+ getId: (cell) => cell.id,
331
+ });
332
+ };
333
+
334
+ it("returns the selected cell target when the cell is part of the deck", () => {
335
+ expect(
336
+ resolve(
337
+ [
338
+ { id: "a", type: "slide" },
339
+ { id: "b", type: "fragment" },
340
+ ],
341
+ 1,
342
+ ),
343
+ ).toEqual({ h: 0, v: 0, f: 0 });
344
+ });
345
+
346
+ it("parks a skipped cell on the closest earlier deck cell", () => {
347
+ expect(
348
+ resolve(
349
+ [
350
+ { id: "a", type: "slide" },
351
+ { id: "skip", type: "skip" },
352
+ { id: "b", type: "slide" },
353
+ ],
354
+ 1,
355
+ ),
356
+ ).toEqual({ h: 0, v: 0, f: -1 });
357
+ });
358
+
359
+ it("falls forward when a skipped cell appears before any real slide", () => {
360
+ expect(
361
+ resolve(
362
+ [
363
+ { id: "skip", type: "skip" },
364
+ { id: "a", type: "slide" },
365
+ ],
366
+ 0,
367
+ ),
368
+ ).toEqual({ h: 0, v: 0, f: -1 });
369
+ });
370
+
371
+ it("returns undefined when there is no real slide to park on", () => {
372
+ expect(resolve([{ id: "skip", type: "skip" }], 0)).toBeUndefined();
373
+ });
374
+ });
375
+
376
+ describe("computeDeckNavigation", () => {
377
+ it("returns null when the deck is already on a non-fragment target", () => {
378
+ expect(
379
+ computeDeckNavigation({ h: 0, v: 0, f: -1 }, { h: 0, v: 0, f: -1 }),
380
+ ).toBeNull();
381
+ });
382
+
383
+ it("returns null when the deck is already on the target fragment", () => {
384
+ expect(
385
+ computeDeckNavigation({ h: 0, v: 0, f: 1 }, { h: 0, v: 0, f: 1 }),
386
+ ).toBeNull();
387
+ });
388
+
389
+ it("navigates to a different stack", () => {
390
+ expect(
391
+ computeDeckNavigation({ h: 0, v: 0, f: -1 }, { h: 2, v: 0, f: -1 }),
392
+ ).toEqual({ h: 2, v: 0, f: -1 });
393
+ });
394
+
395
+ it("navigates to a different subslide within the same stack", () => {
396
+ expect(
397
+ computeDeckNavigation({ h: 1, v: 0, f: -1 }, { h: 1, v: 2, f: -1 }),
398
+ ).toEqual({ h: 1, v: 2, f: -1 });
399
+ });
400
+
401
+ it("collapses revealed fragments when jumping to the parent slide", () => {
402
+ // Regression: previously left `f` unchanged, so the deck would stay on
403
+ // the last-revealed fragment when the user clicked the parent slide in
404
+ // the minimap.
405
+ expect(
406
+ computeDeckNavigation({ h: 0, v: 0, f: 2 }, { h: 0, v: 0, f: -1 }),
407
+ ).toEqual({ h: 0, v: 0, f: -1 });
408
+ });
409
+
410
+ it("collapses fragments when jumping to a parent slide in a different stack", () => {
411
+ expect(
412
+ computeDeckNavigation({ h: 1, v: 0, f: 3 }, { h: 0, v: 0, f: -1 }),
413
+ ).toEqual({ h: 0, v: 0, f: -1 });
414
+ });
415
+
416
+ it("advances to a specific fragment on the same slide", () => {
417
+ expect(
418
+ computeDeckNavigation({ h: 0, v: 0, f: -1 }, { h: 0, v: 0, f: 2 }),
419
+ ).toEqual({ h: 0, v: 0, f: 2 });
420
+ });
421
+
422
+ it("rewinds to an earlier fragment on the same slide", () => {
423
+ expect(
424
+ computeDeckNavigation({ h: 0, v: 0, f: 3 }, { h: 0, v: 0, f: 1 }),
425
+ ).toEqual({ h: 0, v: 0, f: 1 });
426
+ });
427
+
428
+ it("jumps across stacks directly to a fragment", () => {
429
+ expect(
430
+ computeDeckNavigation({ h: 0, v: 0, f: -1 }, { h: 2, v: 1, f: 0 }),
431
+ ).toEqual({ h: 2, v: 1, f: 0 });
432
+ });
433
+ });