@marimo-team/islands 0.23.3-dev43 → 0.23.3-dev45
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.
- package/dist/main.js +2594 -2718
- package/dist/{reveal-component-agH2Be6_.js → reveal-component-Dl4bgjB2.js} +826 -560
- package/dist/{slide-CoAyRjHI.js → slide-form-Lvti-hPv.js} +1477 -763
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/editor/renderers/slides-layout/__tests__/compute-slide-cells.test.ts +150 -0
- package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +298 -0
- package/src/components/editor/renderers/slides-layout/compute-slide-cells.ts +50 -0
- package/src/components/editor/renderers/slides-layout/plugin.tsx +54 -9
- package/src/components/editor/renderers/slides-layout/slides-layout.tsx +30 -12
- package/src/components/editor/renderers/slides-layout/types.ts +31 -3
- package/src/components/editor/renderers/types.ts +2 -0
- package/src/components/slides/__tests__/compose-slides.test.ts +433 -0
- package/src/components/slides/compose-slides.ts +337 -0
- package/src/components/slides/minimap.tsx +133 -12
- package/src/components/slides/reveal-component.tsx +337 -74
- package/src/components/slides/reveal-slides.css +33 -1
- package/src/components/slides/slide-form.tsx +347 -0
- package/src/components/ui/radio-group.tsx +5 -3
- package/src/core/cells/types.ts +2 -0
- package/src/core/layout/layout.ts +6 -2
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
EyeOffIcon,
|
|
5
|
+
LayoutTemplateIcon,
|
|
6
|
+
type LucideIcon,
|
|
7
|
+
Rows2Icon,
|
|
8
|
+
CookieIcon,
|
|
9
|
+
PanelRightCloseIcon,
|
|
10
|
+
PanelRightOpenIcon,
|
|
11
|
+
} from "lucide-react";
|
|
12
|
+
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|
13
|
+
import {
|
|
14
|
+
Select,
|
|
15
|
+
SelectContent,
|
|
16
|
+
SelectItem,
|
|
17
|
+
SelectTrigger,
|
|
18
|
+
SelectValue,
|
|
19
|
+
} from "@/components/ui/select";
|
|
20
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
21
|
+
import type { CellId } from "@/core/cells/ids";
|
|
22
|
+
import { cn } from "@/utils/cn";
|
|
23
|
+
import type {
|
|
24
|
+
DeckTransition,
|
|
25
|
+
SlidesLayout,
|
|
26
|
+
SlideType,
|
|
27
|
+
} from "../editor/renderers/slides-layout/types";
|
|
28
|
+
import { useState } from "react";
|
|
29
|
+
import { Tooltip } from "../ui/tooltip";
|
|
30
|
+
import { Button } from "../ui/button";
|
|
31
|
+
import type { RuntimeCell } from "@/core/cells/types";
|
|
32
|
+
|
|
33
|
+
export const DEFAULT_SLIDE_TYPE: SlideType = "slide";
|
|
34
|
+
export const DEFAULT_DECK_TRANSITION: DeckTransition = "slide";
|
|
35
|
+
const COLLAPSED_CONFIG_WIDTH = 36;
|
|
36
|
+
|
|
37
|
+
export interface SlideTypeOption {
|
|
38
|
+
value: SlideType;
|
|
39
|
+
label: string;
|
|
40
|
+
description: string;
|
|
41
|
+
Icon: LucideIcon;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const SLIDE_TYPE_OPTIONS: readonly SlideTypeOption[] = [
|
|
45
|
+
{
|
|
46
|
+
value: "slide",
|
|
47
|
+
label: "Slide",
|
|
48
|
+
description:
|
|
49
|
+
"A new top-level slide. Advances horizontally with the right arrow.",
|
|
50
|
+
Icon: LayoutTemplateIcon,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
value: "sub-slide",
|
|
54
|
+
label: "Sub-slide",
|
|
55
|
+
description:
|
|
56
|
+
"Stacks vertically under the previous slide. Reached with the down arrow.",
|
|
57
|
+
Icon: Rows2Icon,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
value: "fragment",
|
|
61
|
+
label: "Fragment",
|
|
62
|
+
description: "Reveals step-by-step on the current slide without advancing.",
|
|
63
|
+
Icon: CookieIcon,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
value: "skip",
|
|
67
|
+
label: "Skip",
|
|
68
|
+
description:
|
|
69
|
+
"Hidden from the presentation. Still visible here in the editor.",
|
|
70
|
+
Icon: EyeOffIcon,
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Lookup form of {@link SLIDE_TYPE_OPTIONS} for O(1) access by `SlideType`.
|
|
76
|
+
*/
|
|
77
|
+
export const SLIDE_TYPE_OPTIONS_BY_VALUE: Readonly<
|
|
78
|
+
Record<SlideType, SlideTypeOption>
|
|
79
|
+
> = Object.fromEntries(
|
|
80
|
+
SLIDE_TYPE_OPTIONS.map((option) => [option.value, option]),
|
|
81
|
+
) as Record<SlideType, SlideTypeOption>;
|
|
82
|
+
|
|
83
|
+
interface DeckTransitionOption {
|
|
84
|
+
value: DeckTransition;
|
|
85
|
+
label: string;
|
|
86
|
+
description: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const DECK_TRANSITION_OPTIONS: DeckTransitionOption[] = [
|
|
90
|
+
{ value: "none", label: "None", description: "No animation between slides." },
|
|
91
|
+
{ value: "fade", label: "Fade", description: "Cross-fade between slides." },
|
|
92
|
+
{
|
|
93
|
+
value: "slide",
|
|
94
|
+
label: "Slide",
|
|
95
|
+
description: "Slides move horizontally / vertically.",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
value: "convex",
|
|
99
|
+
label: "Convex",
|
|
100
|
+
description: "Rotate with a convex curve.",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
value: "concave",
|
|
104
|
+
label: "Concave",
|
|
105
|
+
description: "Rotate with a concave curve.",
|
|
106
|
+
},
|
|
107
|
+
{ value: "zoom", label: "Zoom", description: "Zoom into the next slide." },
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
const SlidesForm = ({
|
|
111
|
+
layout,
|
|
112
|
+
setLayout,
|
|
113
|
+
cellId,
|
|
114
|
+
}: {
|
|
115
|
+
layout: SlidesLayout;
|
|
116
|
+
setLayout: (layout: SlidesLayout) => void;
|
|
117
|
+
cellId: CellId;
|
|
118
|
+
}) => {
|
|
119
|
+
return (
|
|
120
|
+
<Tabs defaultValue="slide" className="flex flex-col flex-1 p-3 gap-3">
|
|
121
|
+
<TabsList className="grid grid-cols-2">
|
|
122
|
+
<TabsTrigger value="slide">Slide</TabsTrigger>
|
|
123
|
+
<TabsTrigger value="deck">Deck</TabsTrigger>
|
|
124
|
+
</TabsList>
|
|
125
|
+
<TabsContent value="slide" className="mt-0 flex-1">
|
|
126
|
+
<SlideConfigForm
|
|
127
|
+
layout={layout}
|
|
128
|
+
setLayout={setLayout}
|
|
129
|
+
cellId={cellId}
|
|
130
|
+
/>
|
|
131
|
+
</TabsContent>
|
|
132
|
+
<TabsContent value="deck" className="mt-0 flex-1">
|
|
133
|
+
<DeckConfigForm layout={layout} setLayout={setLayout} />
|
|
134
|
+
</TabsContent>
|
|
135
|
+
</Tabs>
|
|
136
|
+
);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const SlideConfigForm = ({
|
|
140
|
+
layout,
|
|
141
|
+
setLayout,
|
|
142
|
+
cellId,
|
|
143
|
+
}: {
|
|
144
|
+
layout: SlidesLayout;
|
|
145
|
+
setLayout: (layout: SlidesLayout) => void;
|
|
146
|
+
cellId: CellId;
|
|
147
|
+
}) => {
|
|
148
|
+
const currentSlideType: SlideType =
|
|
149
|
+
layout.cells.get(cellId)?.type ?? DEFAULT_SLIDE_TYPE;
|
|
150
|
+
|
|
151
|
+
const handleSlideTypeChange = (value: SlideType) => {
|
|
152
|
+
const existingConfig = layout.cells.get(cellId);
|
|
153
|
+
const newCells = new Map(layout.cells);
|
|
154
|
+
newCells.set(cellId, { ...existingConfig, type: value });
|
|
155
|
+
setLayout({
|
|
156
|
+
...layout,
|
|
157
|
+
cells: newCells,
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<div className="flex flex-col gap-3">
|
|
163
|
+
<span className="font-semibold text-sm">Slide type</span>
|
|
164
|
+
<RadioGroup
|
|
165
|
+
aria-label="Slide type"
|
|
166
|
+
value={currentSlideType}
|
|
167
|
+
onValueChange={(value) => handleSlideTypeChange(value as SlideType)}
|
|
168
|
+
className="flex flex-col gap-1.5"
|
|
169
|
+
>
|
|
170
|
+
{SLIDE_TYPE_OPTIONS.map(({ value, label, description, Icon }) => {
|
|
171
|
+
const isSelected = currentSlideType === value;
|
|
172
|
+
return (
|
|
173
|
+
<RadioGroupItem
|
|
174
|
+
key={value}
|
|
175
|
+
value={value}
|
|
176
|
+
className={cn(
|
|
177
|
+
"group h-auto w-full text-left rounded-md p-2.5 transition-colors shadow-none! border",
|
|
178
|
+
"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
179
|
+
isSelected
|
|
180
|
+
? "border-primary bg-primary/5"
|
|
181
|
+
: "border-border bg-background hover:bg-accent/50 hover:border-foreground/30",
|
|
182
|
+
)}
|
|
183
|
+
>
|
|
184
|
+
<div className="flex items-start gap-2.5">
|
|
185
|
+
<span
|
|
186
|
+
className={cn(
|
|
187
|
+
"mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded",
|
|
188
|
+
isSelected
|
|
189
|
+
? "bg-primary/10 text-primary"
|
|
190
|
+
: "bg-muted text-muted-foreground group-hover:text-foreground",
|
|
191
|
+
)}
|
|
192
|
+
>
|
|
193
|
+
<Icon className="h-3.5 w-3.5" />
|
|
194
|
+
</span>
|
|
195
|
+
<div>
|
|
196
|
+
<p
|
|
197
|
+
className={cn(
|
|
198
|
+
"text-sm font-medium leading-tight",
|
|
199
|
+
isSelected ? "text-primary" : "text-foreground",
|
|
200
|
+
)}
|
|
201
|
+
>
|
|
202
|
+
{label}
|
|
203
|
+
</p>
|
|
204
|
+
<p className="mt-0.5 text-xs text-foreground/70">
|
|
205
|
+
{description}
|
|
206
|
+
</p>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
</RadioGroupItem>
|
|
210
|
+
);
|
|
211
|
+
})}
|
|
212
|
+
</RadioGroup>
|
|
213
|
+
</div>
|
|
214
|
+
);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const DeckConfigForm = ({
|
|
218
|
+
layout,
|
|
219
|
+
setLayout,
|
|
220
|
+
}: {
|
|
221
|
+
layout: SlidesLayout;
|
|
222
|
+
setLayout: (layout: SlidesLayout) => void;
|
|
223
|
+
}) => {
|
|
224
|
+
const currentTransition: DeckTransition =
|
|
225
|
+
layout.deck?.transition ?? DEFAULT_DECK_TRANSITION;
|
|
226
|
+
const activeDescription = DECK_TRANSITION_OPTIONS.find(
|
|
227
|
+
(opt) => opt.value === currentTransition,
|
|
228
|
+
)?.description;
|
|
229
|
+
|
|
230
|
+
const handleTransitionChange = (value: DeckTransition) => {
|
|
231
|
+
setLayout({
|
|
232
|
+
...layout,
|
|
233
|
+
deck: { ...layout.deck, transition: value },
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<div className="flex flex-col gap-3">
|
|
239
|
+
<div className="flex flex-col gap-1.5">
|
|
240
|
+
<label
|
|
241
|
+
htmlFor="deck-transition"
|
|
242
|
+
className="font-semibold text-sm text-foreground"
|
|
243
|
+
>
|
|
244
|
+
Transition
|
|
245
|
+
</label>
|
|
246
|
+
<Select
|
|
247
|
+
value={currentTransition}
|
|
248
|
+
onValueChange={(value) =>
|
|
249
|
+
handleTransitionChange(value as DeckTransition)
|
|
250
|
+
}
|
|
251
|
+
>
|
|
252
|
+
<SelectTrigger id="deck-transition" aria-label="Slide transition">
|
|
253
|
+
<SelectValue />
|
|
254
|
+
</SelectTrigger>
|
|
255
|
+
<SelectContent>
|
|
256
|
+
{DECK_TRANSITION_OPTIONS.map(({ value, label }) => (
|
|
257
|
+
<SelectItem key={value} value={value}>
|
|
258
|
+
{label}
|
|
259
|
+
</SelectItem>
|
|
260
|
+
))}
|
|
261
|
+
</SelectContent>
|
|
262
|
+
</Select>
|
|
263
|
+
{activeDescription && (
|
|
264
|
+
<p className="text-xs text-foreground/70">{activeDescription}</p>
|
|
265
|
+
)}
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
);
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
export const SlideSidebar = ({
|
|
272
|
+
configWidth,
|
|
273
|
+
layout,
|
|
274
|
+
setLayout,
|
|
275
|
+
activeConfigCell,
|
|
276
|
+
}: {
|
|
277
|
+
configWidth: number;
|
|
278
|
+
layout: SlidesLayout;
|
|
279
|
+
setLayout: (layout: SlidesLayout) => void;
|
|
280
|
+
activeConfigCell?: RuntimeCell;
|
|
281
|
+
}) => {
|
|
282
|
+
const [isConfigOpen, setIsConfigOpen] = useState(false);
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<aside
|
|
286
|
+
className="h-full flex flex-col border-l border-border/60 bg-muted/20 transition-[width] duration-200 ease-out overflow-hidden"
|
|
287
|
+
style={{
|
|
288
|
+
width: isConfigOpen ? configWidth : COLLAPSED_CONFIG_WIDTH,
|
|
289
|
+
}}
|
|
290
|
+
aria-label="Slide configuration"
|
|
291
|
+
>
|
|
292
|
+
<header
|
|
293
|
+
className={cn(
|
|
294
|
+
"flex items-center h-9 shrink-0 border-b border-border/60",
|
|
295
|
+
isConfigOpen ? "justify-between px-2" : "justify-center px-0",
|
|
296
|
+
)}
|
|
297
|
+
>
|
|
298
|
+
{isConfigOpen && (
|
|
299
|
+
<span className="text-xs font-medium uppercase tracking-wide text-muted-foreground pl-1">
|
|
300
|
+
Configuration
|
|
301
|
+
</span>
|
|
302
|
+
)}
|
|
303
|
+
<Tooltip content={isConfigOpen ? "Collapse panel" : "Expand panel"}>
|
|
304
|
+
<Button
|
|
305
|
+
variant="ghost"
|
|
306
|
+
size="icon"
|
|
307
|
+
className="h-7 w-7 text-muted-foreground hover:text-foreground"
|
|
308
|
+
onClick={() => setIsConfigOpen(!isConfigOpen)}
|
|
309
|
+
aria-expanded={isConfigOpen}
|
|
310
|
+
aria-controls="slide-config-panel"
|
|
311
|
+
>
|
|
312
|
+
{isConfigOpen ? (
|
|
313
|
+
<PanelRightCloseIcon className="h-4 w-4" />
|
|
314
|
+
) : (
|
|
315
|
+
<PanelRightOpenIcon className="h-4 w-4" />
|
|
316
|
+
)}
|
|
317
|
+
</Button>
|
|
318
|
+
</Tooltip>
|
|
319
|
+
</header>
|
|
320
|
+
|
|
321
|
+
{isConfigOpen && (
|
|
322
|
+
<div
|
|
323
|
+
id="slide-config-panel"
|
|
324
|
+
className="flex-1 overflow-y-auto overflow-x-hidden"
|
|
325
|
+
>
|
|
326
|
+
{activeConfigCell ? (
|
|
327
|
+
<SlidesForm
|
|
328
|
+
layout={layout}
|
|
329
|
+
setLayout={setLayout}
|
|
330
|
+
cellId={activeConfigCell.id}
|
|
331
|
+
/>
|
|
332
|
+
) : (
|
|
333
|
+
<div className="flex flex-col gap-1.5 p-3 text-xs text-muted-foreground">
|
|
334
|
+
<span className="font-semibold text-sm text-foreground">
|
|
335
|
+
No slides yet
|
|
336
|
+
</span>
|
|
337
|
+
<p>
|
|
338
|
+
Run a cell that produces output to add it to the deck. Slide
|
|
339
|
+
settings will appear here once a slide is selected.
|
|
340
|
+
</p>
|
|
341
|
+
</div>
|
|
342
|
+
)}
|
|
343
|
+
</div>
|
|
344
|
+
)}
|
|
345
|
+
</aside>
|
|
346
|
+
);
|
|
347
|
+
};
|
|
@@ -33,9 +33,11 @@ const RadioGroupItem = React.forwardRef<
|
|
|
33
33
|
)}
|
|
34
34
|
{...props}
|
|
35
35
|
>
|
|
36
|
-
|
|
37
|
-
<
|
|
38
|
-
|
|
36
|
+
{children ?? (
|
|
37
|
+
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
|
38
|
+
<Circle className="h-[10px] w-[10px] fill-primary text-current" />
|
|
39
|
+
</RadioGroupPrimitive.Indicator>
|
|
40
|
+
)}
|
|
39
41
|
</RadioGroupPrimitive.Item>
|
|
40
42
|
);
|
|
41
43
|
});
|
package/src/core/cells/types.ts
CHANGED
|
@@ -119,6 +119,8 @@ export interface CellRuntimeState {
|
|
|
119
119
|
serialization?: string | null;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
export type RuntimeCell = CellRuntimeState & CellData;
|
|
123
|
+
|
|
122
124
|
export type WithResponse<T> = T & {
|
|
123
125
|
/**
|
|
124
126
|
* This is not saved to the server, but we update this field
|
|
@@ -84,7 +84,6 @@ export function getSerializedLayout() {
|
|
|
84
84
|
return null;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
const data = layoutData[selectedLayout];
|
|
88
87
|
const plugin = cellRendererPlugins.find(
|
|
89
88
|
(plugin) => plugin.type === selectedLayout,
|
|
90
89
|
);
|
|
@@ -92,8 +91,13 @@ export function getSerializedLayout() {
|
|
|
92
91
|
Logger.error(`Unknown layout type: ${selectedLayout}`);
|
|
93
92
|
return null;
|
|
94
93
|
}
|
|
94
|
+
const cells = notebookCells(notebook);
|
|
95
|
+
// Fall back to the plugin's initial layout when the user has not yet
|
|
96
|
+
// interacted with this layout — otherwise serializers that expect a
|
|
97
|
+
// structured layout object crash on `undefined`.
|
|
98
|
+
const data = layoutData[selectedLayout] ?? plugin.getInitialLayout(cells);
|
|
95
99
|
return {
|
|
96
100
|
type: selectedLayout,
|
|
97
|
-
data: plugin.serializeLayout(data,
|
|
101
|
+
data: plugin.serializeLayout(data, cells),
|
|
98
102
|
};
|
|
99
103
|
}
|