@marimo-team/frontend 0.23.7-dev64 → 0.23.7-dev65
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/assets/{cell-editor-Cgyoqdi5.js → cell-editor-D_oiWxRl.js} +1 -1
- package/dist/assets/{command-palette-n_e11WBA.js → command-palette-mVKHqaan.js} +1 -1
- package/dist/assets/{edit-page-Dos8zz_9.js → edit-page-BgGLD5aB.js} +4 -4
- package/dist/assets/{hooks-7OHHugrQ.js → hooks-DZNVrVar.js} +1 -1
- package/dist/assets/index-5kQk2blA.css +2 -0
- package/dist/assets/{index-1EIgCVR_.js → index-DGkoEAWm.js} +3 -3
- package/dist/assets/layout-QNv3OC6l.js +9 -0
- package/dist/assets/{panels-DvIOAb34.js → panels-D7OK0dYk.js} +1 -1
- package/dist/assets/reveal-component-OLi7V7Sf.js +1069 -0
- package/dist/assets/{run-page-DJOwAe2z.js → run-page-vKjN7tOv.js} +1 -1
- package/dist/assets/{scratchpad-panel-CpM3jVv7.js → scratchpad-panel-H6R60n5E.js} +1 -1
- package/dist/assets/slides-MCbMDtlY.css +1 -0
- package/dist/assets/{useNotebookActions-DqlAe4Ea.js → useNotebookActions-BKENzoCk.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/components/editor/renderers/slides-layout/__tests__/compute-slide-cells.test.ts +5 -4
- package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +55 -15
- package/src/components/editor/renderers/slides-layout/plugin.tsx +8 -25
- package/src/components/editor/renderers/slides-layout/slides-layout.tsx +18 -5
- package/src/components/editor/renderers/slides-layout/types.ts +40 -31
- package/src/components/slides/__tests__/slide-notes.test.ts +131 -0
- package/src/components/slides/reveal-component.tsx +242 -147
- package/src/components/slides/slide-notes-editor.tsx +127 -0
- package/src/components/slides/slide-notes.ts +64 -0
- package/src/components/slides/slides.css +14 -0
- package/src/core/edit-app.tsx +1 -1
- package/dist/assets/index-8ta9aYoc.css +0 -2
- package/dist/assets/layout-DQGNHEpb.js +0 -9
- package/dist/assets/reveal-component-ClM8W-TD.js +0 -91
- package/dist/assets/slides-BVvo79ll.css +0 -1
- /package/dist/assets/{swiper-component-DXTq6N70.js → swiper-component-Da-gZjtJ.js} +0 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import type {
|
|
5
|
+
SlideConfig,
|
|
6
|
+
SlideType,
|
|
7
|
+
} from "@/components/editor/renderers/slides-layout/types";
|
|
8
|
+
import type { CellId } from "@/core/cells/ids";
|
|
9
|
+
import { cellId } from "@/__tests__/branded";
|
|
10
|
+
import { composeSlides } from "../compose-slides";
|
|
11
|
+
import { buildSubslideNotes, collectBlockNotes } from "../slide-notes";
|
|
12
|
+
|
|
13
|
+
interface Cell {
|
|
14
|
+
id: CellId;
|
|
15
|
+
type?: SlideType;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const cell = (id: string, type?: SlideType): Cell => ({
|
|
19
|
+
id: cellId(id),
|
|
20
|
+
type,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const configs = (
|
|
24
|
+
notes: Record<string, string>,
|
|
25
|
+
): ReadonlyMap<CellId, SlideConfig> =>
|
|
26
|
+
new Map(
|
|
27
|
+
Object.entries(notes).map(([id, speakerNotes]) => [
|
|
28
|
+
cellId(id),
|
|
29
|
+
{ speakerNotes } satisfies SlideConfig,
|
|
30
|
+
]),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const firstSubslide = (cells: Cell[]) =>
|
|
34
|
+
composeSlides({ cells, getType: (c) => c.type ?? "slide" }).stacks[0]
|
|
35
|
+
.subslides[0];
|
|
36
|
+
|
|
37
|
+
describe("collectBlockNotes", () => {
|
|
38
|
+
it("concatenates non-empty notes with paragraph spacing", () => {
|
|
39
|
+
const result = collectBlockNotes(
|
|
40
|
+
[cell("a"), cell("b"), cell("c")],
|
|
41
|
+
configs({ a: "first", b: "", c: "third" }),
|
|
42
|
+
);
|
|
43
|
+
expect(result).toBe("first\n\nthird");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("returns an empty string when no cell has notes", () => {
|
|
47
|
+
expect(collectBlockNotes([cell("a")], configs({}))).toBe("");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("ignores whitespace-only notes", () => {
|
|
51
|
+
expect(
|
|
52
|
+
collectBlockNotes([cell("a"), cell("b")], configs({ a: " ", b: "x" })),
|
|
53
|
+
).toBe("x");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("buildSubslideNotes", () => {
|
|
58
|
+
it("returns empty notes when no cell has any", () => {
|
|
59
|
+
const subslide = firstSubslide([cell("a"), cell("b", "fragment")]);
|
|
60
|
+
expect(buildSubslideNotes(subslide, configs({}))).toEqual({
|
|
61
|
+
slideLevel: "",
|
|
62
|
+
cumulativeByBlock: new Map(),
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("returns only slide-level notes when there are no fragments", () => {
|
|
67
|
+
const subslide = firstSubslide([cell("a")]);
|
|
68
|
+
expect(buildSubslideNotes(subslide, configs({ a: "intro" }))).toEqual({
|
|
69
|
+
slideLevel: "intro",
|
|
70
|
+
cumulativeByBlock: new Map(),
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("accumulates fragments below the slide-level notes with a divider", () => {
|
|
75
|
+
const subslide = firstSubslide([
|
|
76
|
+
cell("a"),
|
|
77
|
+
cell("b", "fragment"),
|
|
78
|
+
cell("c", "fragment"),
|
|
79
|
+
]);
|
|
80
|
+
const { slideLevel, cumulativeByBlock } = buildSubslideNotes(
|
|
81
|
+
subslide,
|
|
82
|
+
configs({ a: "intro", b: "step one", c: "step two" }),
|
|
83
|
+
);
|
|
84
|
+
expect(slideLevel).toBe("intro");
|
|
85
|
+
expect(cumulativeByBlock.get(1)).toBe("intro\n\n---\n\nstep one");
|
|
86
|
+
expect(cumulativeByBlock.get(2)).toBe(
|
|
87
|
+
"intro\n\n---\n\nstep one\n\n---\n\nstep two",
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("accumulates fragments with no slide-level notes", () => {
|
|
92
|
+
const subslide = firstSubslide([
|
|
93
|
+
cell("a"),
|
|
94
|
+
cell("b", "fragment"),
|
|
95
|
+
cell("c", "fragment"),
|
|
96
|
+
]);
|
|
97
|
+
const { slideLevel, cumulativeByBlock } = buildSubslideNotes(
|
|
98
|
+
subslide,
|
|
99
|
+
configs({ b: "first reveal", c: "second reveal" }),
|
|
100
|
+
);
|
|
101
|
+
expect(slideLevel).toBe("");
|
|
102
|
+
expect(cumulativeByBlock.get(1)).toBe("first reveal");
|
|
103
|
+
expect(cumulativeByBlock.get(2)).toBe(
|
|
104
|
+
"first reveal\n\n---\n\nsecond reveal",
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("skips empty fragments without leaving dangling dividers", () => {
|
|
109
|
+
const subslide = firstSubslide([
|
|
110
|
+
cell("a"),
|
|
111
|
+
cell("b", "fragment"),
|
|
112
|
+
cell("c", "fragment"),
|
|
113
|
+
]);
|
|
114
|
+
const { cumulativeByBlock } = buildSubslideNotes(
|
|
115
|
+
subslide,
|
|
116
|
+
configs({ a: "intro", c: "third" }),
|
|
117
|
+
);
|
|
118
|
+
expect(cumulativeByBlock.get(1)).toBe("intro");
|
|
119
|
+
expect(cumulativeByBlock.get(2)).toBe("intro\n\n---\n\nthird");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("returns no cumulative entries when fragments and slide have no notes", () => {
|
|
123
|
+
const subslide = firstSubslide([
|
|
124
|
+
cell("a"),
|
|
125
|
+
cell("b", "fragment"),
|
|
126
|
+
cell("c", "fragment"),
|
|
127
|
+
]);
|
|
128
|
+
const { cumulativeByBlock } = buildSubslideNotes(subslide, configs({}));
|
|
129
|
+
expect(cumulativeByBlock.size).toBe(0);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -11,9 +11,11 @@ import {
|
|
|
11
11
|
import useEvent from "react-use-event-hook";
|
|
12
12
|
import { CodeIcon, ExpandIcon, EyeOffIcon } from "lucide-react";
|
|
13
13
|
import { Deck, Fragment, Slide, Stack } from "@revealjs/react";
|
|
14
|
+
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
|
|
14
15
|
import { Slide as CellOutputSlide } from "@/components/slides/slide";
|
|
15
16
|
import { Button } from "@/components/ui/button";
|
|
16
17
|
import { Tooltip } from "@/components/ui/tooltip";
|
|
18
|
+
import type { CellId } from "@/core/cells/ids";
|
|
17
19
|
import type { RuntimeCell } from "@/core/cells/types";
|
|
18
20
|
import type { RevealApi, RevealConfig } from "reveal.js";
|
|
19
21
|
import { useEventListener } from "@/hooks/useEventListener";
|
|
@@ -21,7 +23,10 @@ import { Events } from "@/utils/events";
|
|
|
21
23
|
import { Logger } from "@/utils/Logger";
|
|
22
24
|
import "./slides.css";
|
|
23
25
|
import "./reveal-slides.css";
|
|
24
|
-
import type {
|
|
26
|
+
import type {
|
|
27
|
+
SlideConfig,
|
|
28
|
+
SlidesLayout,
|
|
29
|
+
} from "../editor/renderers/slides-layout/types";
|
|
25
30
|
import {
|
|
26
31
|
buildSlideIndices,
|
|
27
32
|
composeSlides,
|
|
@@ -39,10 +44,14 @@ import {
|
|
|
39
44
|
SlideCellReadOnlyView,
|
|
40
45
|
SlideCellView,
|
|
41
46
|
} from "@/components/slides/slide-cell-view";
|
|
47
|
+
import { SlideNotesEditor } from "./slide-notes-editor";
|
|
48
|
+
import { buildSubslideNotes, NOTES_DIVIDER } from "./slide-notes";
|
|
42
49
|
import { cn } from "@/utils/cn";
|
|
43
50
|
import { isIslands } from "@/core/islands/utils";
|
|
44
51
|
import { useNotebookCodeAvailable } from "@/core/meta/code-visibility";
|
|
45
|
-
import type
|
|
52
|
+
import { type AppMode, kioskModeAtom } from "@/core/mode";
|
|
53
|
+
import { useAtomValue } from "jotai";
|
|
54
|
+
import RevealNotes from "reveal.js/plugin/notes";
|
|
46
55
|
|
|
47
56
|
const ASPECT_RATIO = 16 / 9;
|
|
48
57
|
|
|
@@ -124,56 +133,93 @@ function triggerResize(deck: RevealApi | null) {
|
|
|
124
133
|
}
|
|
125
134
|
}
|
|
126
135
|
|
|
136
|
+
// The speaker view renders this via innerHTML with `white-space: normal`, so
|
|
137
|
+
// we materialize `\n` as `<br>` and a lone `---` line as `<hr>`.
|
|
138
|
+
const NotesAside = ({ text }: { text: string }) => {
|
|
139
|
+
const lines = text.split("\n");
|
|
140
|
+
return (
|
|
141
|
+
<aside className="notes">
|
|
142
|
+
{lines.map((line, idx) => {
|
|
143
|
+
const isLast = idx === lines.length - 1;
|
|
144
|
+
if (line === NOTES_DIVIDER) {
|
|
145
|
+
return <hr key={idx} />;
|
|
146
|
+
}
|
|
147
|
+
return (
|
|
148
|
+
<ReactFragment key={idx}>
|
|
149
|
+
{line}
|
|
150
|
+
{!isLast && <br />}
|
|
151
|
+
</ReactFragment>
|
|
152
|
+
);
|
|
153
|
+
})}
|
|
154
|
+
</aside>
|
|
155
|
+
);
|
|
156
|
+
};
|
|
157
|
+
|
|
127
158
|
const SubslideView = ({
|
|
128
159
|
subslide,
|
|
129
160
|
showCode,
|
|
130
161
|
isEditable,
|
|
162
|
+
slideConfigs,
|
|
131
163
|
}: {
|
|
132
164
|
subslide: ComposedSubslide<RuntimeCell>;
|
|
133
165
|
showCode: boolean;
|
|
134
166
|
isEditable: boolean;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
167
|
+
slideConfigs: ReadonlyMap<CellId, SlideConfig>;
|
|
168
|
+
}) => {
|
|
169
|
+
const { slideLevel, cumulativeByBlock } = buildSubslideNotes(
|
|
170
|
+
subslide,
|
|
171
|
+
slideConfigs,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<Slide>
|
|
176
|
+
<div className="h-full w-full overflow-auto flex">
|
|
177
|
+
<div
|
|
178
|
+
className={
|
|
179
|
+
showCode
|
|
180
|
+
? "mo-slide-content flex flex-col gap-3"
|
|
181
|
+
: "mo-slide-content"
|
|
182
|
+
}
|
|
183
|
+
style={{
|
|
184
|
+
margin: "auto 20px",
|
|
185
|
+
}}
|
|
186
|
+
>
|
|
187
|
+
{subslide.blocks.map((block, i) => {
|
|
188
|
+
const rendered = block.cells.map((cell) => {
|
|
189
|
+
if (!showCode) {
|
|
190
|
+
return (
|
|
191
|
+
<CellOutputSlide
|
|
192
|
+
key={cell.id}
|
|
193
|
+
cellId={cell.id}
|
|
194
|
+
status={cell.status}
|
|
195
|
+
output={cell.output}
|
|
196
|
+
/>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
return isEditable ? (
|
|
200
|
+
<SlideCellView key={cell.id} cell={cell} />
|
|
201
|
+
) : (
|
|
202
|
+
<SlideCellReadOnlyView key={cell.id} cell={cell} />
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
if (block.isFragment) {
|
|
206
|
+
const cumulative = cumulativeByBlock.get(i);
|
|
149
207
|
return (
|
|
150
|
-
<
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
output={cell.output}
|
|
155
|
-
/>
|
|
208
|
+
<Fragment key={i} as="div">
|
|
209
|
+
{rendered}
|
|
210
|
+
{cumulative && <NotesAside text={cumulative} />}
|
|
211
|
+
</Fragment>
|
|
156
212
|
);
|
|
157
213
|
}
|
|
158
|
-
return
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
<SlideCellReadOnlyView key={cell.id} cell={cell} />
|
|
162
|
-
);
|
|
163
|
-
});
|
|
164
|
-
if (block.isFragment) {
|
|
165
|
-
return (
|
|
166
|
-
<Fragment key={i} as="div">
|
|
167
|
-
{rendered}
|
|
168
|
-
</Fragment>
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
return <ReactFragment key={i}>{rendered}</ReactFragment>;
|
|
172
|
-
})}
|
|
214
|
+
return <ReactFragment key={i}>{rendered}</ReactFragment>;
|
|
215
|
+
})}
|
|
216
|
+
</div>
|
|
173
217
|
</div>
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
218
|
+
{/* Outside any `.fragment`: shown only before any fragment is revealed. */}
|
|
219
|
+
{slideLevel && <NotesAside text={slideLevel} />}
|
|
220
|
+
</Slide>
|
|
221
|
+
);
|
|
222
|
+
};
|
|
177
223
|
|
|
178
224
|
// There is an upstream react bug in dev mode (https://github.com/facebook/react/issues/34840)
|
|
179
225
|
// Uncaught SecurityError: Failed to read a named property '$$typeof' from 'Window'
|
|
@@ -200,6 +246,13 @@ const RevealSlidesComponent = ({
|
|
|
200
246
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
201
247
|
const deckRef = useRef<RevealApi | null>(null);
|
|
202
248
|
const { width, height } = useSlideDimensions(containerRef);
|
|
249
|
+
// Skip the Notes plugin inside reveal's own speaker-view iframes so pressing
|
|
250
|
+
// `S` there doesn't try to spawn another popup.
|
|
251
|
+
const kioskMode = useAtomValue(kioskModeAtom);
|
|
252
|
+
const deckPlugins = useMemo(
|
|
253
|
+
() => (kioskMode ? [] : [RevealNotes]),
|
|
254
|
+
[kioskMode],
|
|
255
|
+
);
|
|
203
256
|
|
|
204
257
|
const [showCode, setShowCode] = useState(false);
|
|
205
258
|
const codeAvailable = useNotebookCodeAvailable(cellsWithOutput);
|
|
@@ -241,6 +294,16 @@ const RevealSlidesComponent = ({
|
|
|
241
294
|
);
|
|
242
295
|
|
|
243
296
|
const deckTransition = layout.deck?.transition ?? DEFAULT_DECK_TRANSITION;
|
|
297
|
+
// Reveal's Notes plugin iframes the deck for the current/upcoming-slide
|
|
298
|
+
// previews. We load the same URL but as a read-only kiosk client with the
|
|
299
|
+
// app chrome hidden, which `<SlidesLayoutRenderer>` interprets the same as
|
|
300
|
+
// read mode (no minimap, sidebar, or notes editor).
|
|
301
|
+
const kioskUrl = useMemo(() => {
|
|
302
|
+
const url = new URL(window.location.href);
|
|
303
|
+
url.searchParams.set("kiosk", "true");
|
|
304
|
+
url.searchParams.set("show-chrome", "false");
|
|
305
|
+
return url.toString();
|
|
306
|
+
}, []);
|
|
244
307
|
const revealConfig: RevealConfig = useMemo(
|
|
245
308
|
() => ({
|
|
246
309
|
embedded: true,
|
|
@@ -251,8 +314,9 @@ const RevealSlidesComponent = ({
|
|
|
251
314
|
maxScale: 2,
|
|
252
315
|
transition: deckTransition,
|
|
253
316
|
keyboardCondition: (event: KeyboardEvent) => !Events.fromInput(event),
|
|
317
|
+
url: kioskUrl,
|
|
254
318
|
}),
|
|
255
|
-
[width, height, deckTransition],
|
|
319
|
+
[width, height, deckTransition, kioskUrl],
|
|
256
320
|
);
|
|
257
321
|
|
|
258
322
|
const navigateDeckToActiveCell = useEvent((deck: RevealApi) => {
|
|
@@ -357,126 +421,157 @@ const RevealSlidesComponent = ({
|
|
|
357
421
|
|
|
358
422
|
useEventListener(document, "keydown", handleParkedNavKey, { capture: true });
|
|
359
423
|
|
|
360
|
-
|
|
361
|
-
<div
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
>
|
|
366
|
-
<
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
return (
|
|
381
|
-
<SubslideView
|
|
382
|
-
key={h}
|
|
383
|
-
subslide={stack.subslides[0]}
|
|
384
|
-
showCode={codeShown && isActive}
|
|
385
|
-
isEditable={isEditable}
|
|
386
|
-
/>
|
|
387
|
-
);
|
|
388
|
-
}
|
|
424
|
+
const slideArea = (
|
|
425
|
+
<div
|
|
426
|
+
ref={containerRef}
|
|
427
|
+
className="h-full w-full min-w-0 flex items-center justify-center overflow-hidden"
|
|
428
|
+
>
|
|
429
|
+
<div className="group relative" style={{ width, height }}>
|
|
430
|
+
<Deck
|
|
431
|
+
deckRef={deckRef}
|
|
432
|
+
className="aspect-video w-full overflow-hidden border rounded bg-background mo-slides-theme prose-slides"
|
|
433
|
+
config={revealConfig}
|
|
434
|
+
onReady={handleDeckReady}
|
|
435
|
+
onSlideChange={handleSlideChange}
|
|
436
|
+
onFragmentShown={reportCurrentCell}
|
|
437
|
+
onFragmentHidden={reportCurrentCell}
|
|
438
|
+
plugins={deckPlugins}
|
|
439
|
+
>
|
|
440
|
+
{composition.stacks.map((stack, h) => {
|
|
441
|
+
if (stack.subslides.length === 1) {
|
|
442
|
+
const isActive =
|
|
443
|
+
activeSubslide?.h === h && activeSubslide?.v === 0;
|
|
389
444
|
return (
|
|
390
|
-
<
|
|
391
|
-
{
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
subslide={sub}
|
|
398
|
-
showCode={codeShown && isActive}
|
|
399
|
-
isEditable={isEditable}
|
|
400
|
-
/>
|
|
401
|
-
);
|
|
402
|
-
})}
|
|
403
|
-
</Stack>
|
|
445
|
+
<SubslideView
|
|
446
|
+
key={h}
|
|
447
|
+
subslide={stack.subslides[0]}
|
|
448
|
+
showCode={codeShown && isActive}
|
|
449
|
+
isEditable={isEditable}
|
|
450
|
+
slideConfigs={layout.cells}
|
|
451
|
+
/>
|
|
404
452
|
);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
453
|
+
}
|
|
454
|
+
return (
|
|
455
|
+
<Stack key={h}>
|
|
456
|
+
{stack.subslides.map((sub, v) => {
|
|
457
|
+
const isActive =
|
|
458
|
+
activeSubslide?.h === h && activeSubslide?.v === v;
|
|
459
|
+
return (
|
|
460
|
+
<SubslideView
|
|
461
|
+
key={v}
|
|
462
|
+
subslide={sub}
|
|
463
|
+
showCode={codeShown && isActive}
|
|
464
|
+
isEditable={isEditable}
|
|
465
|
+
slideConfigs={layout.cells}
|
|
466
|
+
/>
|
|
467
|
+
);
|
|
468
|
+
})}
|
|
469
|
+
</Stack>
|
|
470
|
+
);
|
|
471
|
+
})}
|
|
472
|
+
</Deck>
|
|
473
|
+
{skippedPreviewCell && (
|
|
474
|
+
<div
|
|
475
|
+
className="absolute inset-0 z-10 border rounded bg-background flex flex-col overflow-hidden"
|
|
476
|
+
aria-label="Skipped in presentation"
|
|
477
|
+
>
|
|
478
|
+
<div className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground border-b bg-muted/40">
|
|
479
|
+
<EyeOffIcon className="h-3.5 w-3.5" />
|
|
480
|
+
<span>Skipped in presentation</span>
|
|
481
|
+
</div>
|
|
482
|
+
<div className="flex-1 overflow-auto flex">
|
|
483
|
+
<div className="mo-slide-content" style={{ margin: "auto 20px" }}>
|
|
484
|
+
<CellOutputSlide
|
|
485
|
+
cellId={skippedPreviewCell.id}
|
|
486
|
+
status={skippedPreviewCell.status}
|
|
487
|
+
output={skippedPreviewCell.output}
|
|
488
|
+
/>
|
|
427
489
|
</div>
|
|
428
490
|
</div>
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
data-testid="marimo-plugin-slides-toggle-code"
|
|
435
|
-
variant="ghost"
|
|
436
|
-
size="icon"
|
|
437
|
-
className={cn(
|
|
438
|
-
"text-muted-foreground h-7 w-7",
|
|
439
|
-
codeShown && "text-foreground bg-muted",
|
|
440
|
-
)}
|
|
441
|
-
aria-pressed={codeShown}
|
|
442
|
-
aria-label={codeShown ? "Hide code" : "Show code"}
|
|
443
|
-
onClick={toggleShowCode}
|
|
444
|
-
>
|
|
445
|
-
<CodeIcon className="h-4 w-4" />
|
|
446
|
-
</Button>
|
|
447
|
-
</Tooltip>
|
|
448
|
-
)}
|
|
449
|
-
<Tooltip content="Fullscreen (F)">
|
|
491
|
+
</div>
|
|
492
|
+
)}
|
|
493
|
+
<div className="absolute top-2 right-2 z-20 opacity-0 group-hover:opacity-70 text-muted-foreground transition-opacity">
|
|
494
|
+
{codeToggleEnabled && (
|
|
495
|
+
<Tooltip content={codeShown ? "Hide code (C)" : "Show code (C)"}>
|
|
450
496
|
<Button
|
|
451
|
-
data-testid="marimo-plugin-slides-
|
|
497
|
+
data-testid="marimo-plugin-slides-toggle-code"
|
|
452
498
|
variant="ghost"
|
|
453
499
|
size="icon"
|
|
454
|
-
className=
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
Logger.error("Failed to request fullscreen", error);
|
|
462
|
-
});
|
|
463
|
-
}}
|
|
500
|
+
className={cn(
|
|
501
|
+
"text-muted-foreground h-7 w-7",
|
|
502
|
+
codeShown && "text-foreground bg-muted",
|
|
503
|
+
)}
|
|
504
|
+
aria-pressed={codeShown}
|
|
505
|
+
aria-label={codeShown ? "Hide code" : "Show code"}
|
|
506
|
+
onClick={toggleShowCode}
|
|
464
507
|
>
|
|
465
|
-
<
|
|
508
|
+
<CodeIcon className="h-4 w-4" />
|
|
466
509
|
</Button>
|
|
467
510
|
</Tooltip>
|
|
468
|
-
|
|
511
|
+
)}
|
|
512
|
+
<Tooltip content="Fullscreen (F)">
|
|
513
|
+
<Button
|
|
514
|
+
data-testid="marimo-plugin-slides-fullscreen"
|
|
515
|
+
variant="ghost"
|
|
516
|
+
size="icon"
|
|
517
|
+
className="text-muted-foreground h-7 w-7"
|
|
518
|
+
aria-label="Enter fullscreen"
|
|
519
|
+
onClick={() => {
|
|
520
|
+
deckRef.current
|
|
521
|
+
?.getViewportElement()
|
|
522
|
+
?.requestFullscreen()
|
|
523
|
+
.catch((error) => {
|
|
524
|
+
Logger.error("Failed to request fullscreen", error);
|
|
525
|
+
});
|
|
526
|
+
}}
|
|
527
|
+
>
|
|
528
|
+
<ExpandIcon className="h-4 w-4" />
|
|
529
|
+
</Button>
|
|
530
|
+
</Tooltip>
|
|
469
531
|
</div>
|
|
470
532
|
</div>
|
|
533
|
+
</div>
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
if (mode === "read") {
|
|
537
|
+
return (
|
|
538
|
+
<div className="flex-1 min-w-0 flex flex-row gap-3">{slideArea}</div>
|
|
539
|
+
);
|
|
540
|
+
}
|
|
471
541
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
542
|
+
return (
|
|
543
|
+
<div className="flex-1 min-w-0 flex flex-row gap-3">
|
|
544
|
+
<PanelGroup
|
|
545
|
+
direction="vertical"
|
|
546
|
+
autoSaveId="marimo:slides:notes-panel"
|
|
547
|
+
className="flex-1 min-w-0"
|
|
548
|
+
>
|
|
549
|
+
<Panel defaultSize={92} minSize={60}>
|
|
550
|
+
{slideArea}
|
|
551
|
+
</Panel>
|
|
552
|
+
<PanelResizeHandle
|
|
553
|
+
className="mo-slides-notes-resize"
|
|
554
|
+
hitAreaMargins={{ coarse: 12, fine: 4 }}
|
|
478
555
|
/>
|
|
479
|
-
|
|
556
|
+
<Panel
|
|
557
|
+
defaultSize={10}
|
|
558
|
+
minSize={4}
|
|
559
|
+
collapsible={true}
|
|
560
|
+
collapsedSize={4}
|
|
561
|
+
>
|
|
562
|
+
<SlideNotesEditor
|
|
563
|
+
layout={layout}
|
|
564
|
+
setLayout={setLayout}
|
|
565
|
+
cellId={activeConfigCell?.id}
|
|
566
|
+
/>
|
|
567
|
+
</Panel>
|
|
568
|
+
</PanelGroup>
|
|
569
|
+
<SlideSidebar
|
|
570
|
+
configWidth={configWidth}
|
|
571
|
+
layout={layout}
|
|
572
|
+
setLayout={setLayout}
|
|
573
|
+
activeConfigCell={activeConfigCell}
|
|
574
|
+
/>
|
|
480
575
|
</div>
|
|
481
576
|
);
|
|
482
577
|
};
|