@marimo-team/islands 0.23.5-dev5 → 0.23.5-dev7
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/{slide-form-CYU9AOO4.js → code-visibility-B8JChlY3.js} +835 -777
- package/dist/main.js +1039 -1073
- package/dist/{reveal-component-DK-5_Ei4.js → reveal-component-DVr24sXs.js} +575 -527
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/editor/file-tree/file-explorer.tsx +38 -8
- package/src/components/editor/file-tree/file-operations.tsx +19 -2
- package/src/components/editor/notebook-cell.tsx +33 -23
- package/src/components/editor/renderers/slides-layout/slides-layout.tsx +3 -5
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +2 -17
- package/src/components/slides/reveal-component.tsx +159 -47
- package/src/components/slides/reveal-slides.css +8 -0
- package/src/components/slides/slide-cell-view.tsx +182 -0
- package/src/components/slides/slide-form.tsx +2 -0
- package/src/core/cells/utils.ts +45 -0
- package/src/core/islands/stubs/slide-cell-view.tsx +30 -0
- package/src/core/meta/__tests__/code-visibility.test.tsx +141 -0
- package/src/core/meta/code-visibility.ts +48 -0
package/package.json
CHANGED
|
@@ -523,7 +523,10 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
|
|
|
523
523
|
iconClassName="w-5 h-5"
|
|
524
524
|
>
|
|
525
525
|
{!node.data.isDirectory && (
|
|
526
|
-
<DropdownMenuItem
|
|
526
|
+
<DropdownMenuItem
|
|
527
|
+
onSelect={() => node.select()}
|
|
528
|
+
data-testid="file-explorer-open-file-menu-item"
|
|
529
|
+
>
|
|
527
530
|
<ViewIcon className={MENU_ITEM_ICON_CLASS} />
|
|
528
531
|
Open file
|
|
529
532
|
</DropdownMenuItem>
|
|
@@ -533,6 +536,7 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
|
|
|
533
536
|
onSelect={() => {
|
|
534
537
|
openFile({ path: node.data.path });
|
|
535
538
|
}}
|
|
539
|
+
data-testid="file-explorer-open-external-menu-item"
|
|
536
540
|
>
|
|
537
541
|
<ExternalLinkIcon className={MENU_ITEM_ICON_CLASS} />
|
|
538
542
|
Open file in external editor
|
|
@@ -540,28 +544,44 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
|
|
|
540
544
|
)}
|
|
541
545
|
{node.data.isDirectory && (
|
|
542
546
|
<>
|
|
543
|
-
<DropdownMenuItem
|
|
547
|
+
<DropdownMenuItem
|
|
548
|
+
onSelect={() => handleCreateNotebook()}
|
|
549
|
+
data-testid="file-explorer-create-notebook-menu-item"
|
|
550
|
+
>
|
|
544
551
|
<MarimoPlusIcon className={MENU_ITEM_ICON_CLASS} />
|
|
545
552
|
Create notebook
|
|
546
553
|
</DropdownMenuItem>
|
|
547
|
-
<DropdownMenuItem
|
|
554
|
+
<DropdownMenuItem
|
|
555
|
+
onSelect={() => handleCreateFile()}
|
|
556
|
+
data-testid="file-explorer-create-file-menu-item"
|
|
557
|
+
>
|
|
548
558
|
<FilePlus2Icon className={MENU_ITEM_ICON_CLASS} />
|
|
549
559
|
Create file
|
|
550
560
|
</DropdownMenuItem>
|
|
551
|
-
<DropdownMenuItem
|
|
561
|
+
<DropdownMenuItem
|
|
562
|
+
onSelect={() => handleCreateFolder()}
|
|
563
|
+
data-testid="file-explorer-create-folder-menu-item"
|
|
564
|
+
>
|
|
552
565
|
<FolderPlusIcon className={MENU_ITEM_ICON_CLASS} />
|
|
553
566
|
Create folder
|
|
554
567
|
</DropdownMenuItem>
|
|
555
568
|
<DropdownMenuSeparator />
|
|
556
569
|
</>
|
|
557
570
|
)}
|
|
558
|
-
<RenameMenuItem
|
|
559
|
-
|
|
571
|
+
<RenameMenuItem
|
|
572
|
+
onSelect={() => node.edit()}
|
|
573
|
+
testId="file-explorer-rename-menu-item"
|
|
574
|
+
/>
|
|
575
|
+
<DuplicateMenuItem
|
|
576
|
+
onSelect={handleDuplicate}
|
|
577
|
+
testId="file-explorer-duplicate-menu-item"
|
|
578
|
+
/>
|
|
560
579
|
<DropdownMenuItem
|
|
561
580
|
onSelect={async () => {
|
|
562
581
|
await copyToClipboard(node.data.path);
|
|
563
582
|
toast({ title: "Copied to clipboard" });
|
|
564
583
|
}}
|
|
584
|
+
data-testid="file-explorer-copy-path-menu-item"
|
|
565
585
|
>
|
|
566
586
|
<ListTreeIcon className={MENU_ITEM_ICON_CLASS} />
|
|
567
587
|
Copy path
|
|
@@ -574,6 +594,7 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
|
|
|
574
594
|
);
|
|
575
595
|
toast({ title: "Copied to clipboard" });
|
|
576
596
|
}}
|
|
597
|
+
data-testid="file-explorer-copy-relative-path-menu-item"
|
|
577
598
|
>
|
|
578
599
|
<ListTreeIcon className={MENU_ITEM_ICON_CLASS} />
|
|
579
600
|
Copy relative path
|
|
@@ -586,6 +607,7 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
|
|
|
586
607
|
const pythonCode = PYTHON_CODE_FOR_FILE_TYPE[fileType](path);
|
|
587
608
|
handleInsertCode(pythonCode);
|
|
588
609
|
}}
|
|
610
|
+
data-testid="file-explorer-insert-snippet-menu-item"
|
|
589
611
|
>
|
|
590
612
|
<BetweenHorizontalStartIcon className={MENU_ITEM_ICON_CLASS} />
|
|
591
613
|
Insert snippet for reading file
|
|
@@ -601,6 +623,7 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
|
|
|
601
623
|
const pythonCode = PYTHON_CODE_FOR_FILE_TYPE[fileType](path);
|
|
602
624
|
await copyToClipboard(pythonCode);
|
|
603
625
|
}}
|
|
626
|
+
data-testid="file-explorer-copy-snippet-menu-item"
|
|
604
627
|
>
|
|
605
628
|
<BracesIcon className={MENU_ITEM_ICON_CLASS} />
|
|
606
629
|
Copy snippet for reading file
|
|
@@ -608,7 +631,10 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
|
|
|
608
631
|
{node.data.isMarimoFile && !isWasm() && (
|
|
609
632
|
<>
|
|
610
633
|
<DropdownMenuSeparator />
|
|
611
|
-
<DropdownMenuItem
|
|
634
|
+
<DropdownMenuItem
|
|
635
|
+
onSelect={handleOpenMarimoFile}
|
|
636
|
+
data-testid="file-explorer-open-notebook-menu-item"
|
|
637
|
+
>
|
|
612
638
|
<PlaySquareIcon className={MENU_ITEM_ICON_CLASS} />
|
|
613
639
|
Open notebook
|
|
614
640
|
</DropdownMenuItem>
|
|
@@ -637,6 +663,7 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
|
|
|
637
663
|
);
|
|
638
664
|
}
|
|
639
665
|
}}
|
|
666
|
+
data-testid="file-explorer-download-menu-item"
|
|
640
667
|
>
|
|
641
668
|
<DownloadIcon className={MENU_ITEM_ICON_CLASS} />
|
|
642
669
|
Download
|
|
@@ -644,7 +671,10 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
|
|
|
644
671
|
<DropdownMenuSeparator />
|
|
645
672
|
</>
|
|
646
673
|
)}
|
|
647
|
-
<DeleteMenuItem
|
|
674
|
+
<DeleteMenuItem
|
|
675
|
+
onSelect={handleDeleteFile}
|
|
676
|
+
testId="file-explorer-delete-menu-item"
|
|
677
|
+
/>
|
|
648
678
|
</FileActionsDropdown>
|
|
649
679
|
</span>
|
|
650
680
|
</div>
|
|
@@ -219,12 +219,19 @@ export const RenameMenuItem = ({
|
|
|
219
219
|
onSelect,
|
|
220
220
|
disabled,
|
|
221
221
|
title,
|
|
222
|
+
testId,
|
|
222
223
|
}: {
|
|
223
224
|
onSelect: (evt: Event) => void;
|
|
224
225
|
disabled?: boolean;
|
|
225
226
|
title?: string;
|
|
227
|
+
testId?: string;
|
|
226
228
|
}) => (
|
|
227
|
-
<DropdownMenuItem
|
|
229
|
+
<DropdownMenuItem
|
|
230
|
+
onSelect={onSelect}
|
|
231
|
+
disabled={disabled}
|
|
232
|
+
title={title}
|
|
233
|
+
data-testid={testId}
|
|
234
|
+
>
|
|
228
235
|
<Edit3Icon className={MENU_ITEM_ICON_CLASS} />
|
|
229
236
|
Rename
|
|
230
237
|
</DropdownMenuItem>
|
|
@@ -234,12 +241,19 @@ export const DuplicateMenuItem = ({
|
|
|
234
241
|
onSelect,
|
|
235
242
|
disabled,
|
|
236
243
|
title,
|
|
244
|
+
testId,
|
|
237
245
|
}: {
|
|
238
246
|
onSelect: (evt: Event) => void;
|
|
239
247
|
disabled?: boolean;
|
|
240
248
|
title?: string;
|
|
249
|
+
testId?: string;
|
|
241
250
|
}) => (
|
|
242
|
-
<DropdownMenuItem
|
|
251
|
+
<DropdownMenuItem
|
|
252
|
+
onSelect={onSelect}
|
|
253
|
+
disabled={disabled}
|
|
254
|
+
title={title}
|
|
255
|
+
data-testid={testId}
|
|
256
|
+
>
|
|
243
257
|
<CopyIcon className={MENU_ITEM_ICON_CLASS} />
|
|
244
258
|
Duplicate
|
|
245
259
|
</DropdownMenuItem>
|
|
@@ -249,16 +263,19 @@ export const DeleteMenuItem = ({
|
|
|
249
263
|
onSelect,
|
|
250
264
|
disabled,
|
|
251
265
|
title,
|
|
266
|
+
testId,
|
|
252
267
|
}: {
|
|
253
268
|
onSelect: (evt: Event) => void;
|
|
254
269
|
disabled?: boolean;
|
|
255
270
|
title?: string;
|
|
271
|
+
testId?: string;
|
|
256
272
|
}) => (
|
|
257
273
|
<DropdownMenuItem
|
|
258
274
|
onSelect={onSelect}
|
|
259
275
|
variant="danger"
|
|
260
276
|
disabled={disabled}
|
|
261
277
|
title={title}
|
|
278
|
+
data-testid={testId}
|
|
262
279
|
>
|
|
263
280
|
<Trash2Icon className={MENU_ITEM_ICON_CLASS} />
|
|
264
281
|
Delete
|
|
@@ -48,7 +48,11 @@ import {
|
|
|
48
48
|
useCellRuntime,
|
|
49
49
|
} from "../../core/cells/cells";
|
|
50
50
|
import { type CellId, SETUP_CELL_ID } from "../../core/cells/ids";
|
|
51
|
-
import {
|
|
51
|
+
import {
|
|
52
|
+
cellNeedsRun,
|
|
53
|
+
cellStatusClasses,
|
|
54
|
+
isUninstantiated,
|
|
55
|
+
} from "../../core/cells/utils";
|
|
52
56
|
import type { UserConfig } from "../../core/config/config-schema";
|
|
53
57
|
import { isAppInteractionDisabled } from "../../core/websocket/connection-utils";
|
|
54
58
|
import { useCellRenderCount } from "../../hooks/useCellRenderCount";
|
|
@@ -390,9 +394,6 @@ const EditableCellComponent = ({
|
|
|
390
394
|
|
|
391
395
|
const [languageAdapter, setLanguageAdapter] = useState<LanguageAdapterType>();
|
|
392
396
|
|
|
393
|
-
const disabledOrAncestorDisabled =
|
|
394
|
-
cellData.config.disabled || cellRuntime.status === "disabled-transitively";
|
|
395
|
-
|
|
396
397
|
const uninstantiated = isUninstantiated({
|
|
397
398
|
executionTime: cellRuntime.runElapsedTimeMs ?? cellData.lastExecutionTime,
|
|
398
399
|
status: cellRuntime.status,
|
|
@@ -401,10 +402,13 @@ const EditableCellComponent = ({
|
|
|
401
402
|
stopped: cellRuntime.stopped,
|
|
402
403
|
});
|
|
403
404
|
|
|
404
|
-
const needsRun =
|
|
405
|
-
cellData.edited
|
|
406
|
-
cellRuntime.interrupted
|
|
407
|
-
|
|
405
|
+
const needsRun = cellNeedsRun({
|
|
406
|
+
edited: cellData.edited,
|
|
407
|
+
interrupted: cellRuntime.interrupted,
|
|
408
|
+
staleInputs: cellRuntime.staleInputs,
|
|
409
|
+
disabled: cellData.config.disabled,
|
|
410
|
+
status: cellRuntime.status,
|
|
411
|
+
});
|
|
408
412
|
|
|
409
413
|
const loading = outputIsLoading(cellRuntime.status);
|
|
410
414
|
|
|
@@ -532,11 +536,13 @@ const EditableCellComponent = ({
|
|
|
532
536
|
|
|
533
537
|
const className = clsx("marimo-cell", "hover-actions-parent z-10", {
|
|
534
538
|
interactive: true,
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
539
|
+
...cellStatusClasses({
|
|
540
|
+
needsRun,
|
|
541
|
+
errored: cellRuntime.errored,
|
|
542
|
+
stopped: cellRuntime.stopped,
|
|
543
|
+
disabled: cellData.config.disabled,
|
|
544
|
+
status: cellRuntime.status,
|
|
545
|
+
}),
|
|
540
546
|
borderless:
|
|
541
547
|
isMarkdownCodeHidden && hasOutput && !navigationProps["data-selected"],
|
|
542
548
|
});
|
|
@@ -979,9 +985,6 @@ const SetupCellComponent = ({
|
|
|
979
985
|
const setAiCompletionCell = useSetAtom(aiCompletionCellAtom);
|
|
980
986
|
const runCell = useRunCell(cellId);
|
|
981
987
|
|
|
982
|
-
const disabledOrAncestorDisabled =
|
|
983
|
-
cellData.config.disabled || cellRuntime.status === "disabled-transitively";
|
|
984
|
-
|
|
985
988
|
const uninstantiated = isUninstantiated({
|
|
986
989
|
executionTime: cellRuntime.runElapsedTimeMs ?? cellData.lastExecutionTime,
|
|
987
990
|
status: cellRuntime.status,
|
|
@@ -990,10 +993,13 @@ const SetupCellComponent = ({
|
|
|
990
993
|
stopped: cellRuntime.stopped,
|
|
991
994
|
});
|
|
992
995
|
|
|
993
|
-
const needsRun =
|
|
994
|
-
cellData.edited
|
|
995
|
-
cellRuntime.interrupted
|
|
996
|
-
|
|
996
|
+
const needsRun = cellNeedsRun({
|
|
997
|
+
edited: cellData.edited,
|
|
998
|
+
interrupted: cellRuntime.interrupted,
|
|
999
|
+
staleInputs: cellRuntime.staleInputs,
|
|
1000
|
+
disabled: cellData.config.disabled,
|
|
1001
|
+
status: cellRuntime.status,
|
|
1002
|
+
});
|
|
997
1003
|
const loading =
|
|
998
1004
|
cellRuntime.status === "running" || cellRuntime.status === "queued";
|
|
999
1005
|
|
|
@@ -1034,9 +1040,13 @@ const SetupCellComponent = ({
|
|
|
1034
1040
|
|
|
1035
1041
|
const className = clsx("marimo-cell", "hover-actions-parent z-10", {
|
|
1036
1042
|
interactive: true,
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1043
|
+
...cellStatusClasses({
|
|
1044
|
+
needsRun,
|
|
1045
|
+
errored: cellRuntime.errored,
|
|
1046
|
+
stopped: cellRuntime.stopped,
|
|
1047
|
+
disabled: cellData.config.disabled,
|
|
1048
|
+
status: cellRuntime.status,
|
|
1049
|
+
}),
|
|
1040
1050
|
});
|
|
1041
1051
|
|
|
1042
1052
|
const handleRefactorWithAI: OnRefactorWithAI = useEvent(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
import React, { useMemo,
|
|
2
|
+
import React, { useMemo, useState } from "react";
|
|
3
3
|
import { useAtomValue } from "jotai";
|
|
4
4
|
import { numColumnsAtom } from "@/core/cells/cells";
|
|
5
5
|
import type { CellId } from "@/core/cells/ids";
|
|
@@ -8,7 +8,6 @@ import type { SlidesLayout } from "./types";
|
|
|
8
8
|
import { computeSlideCellsInfo } from "./compute-slide-cells";
|
|
9
9
|
import { SlidesMinimap } from "@/components/slides/minimap";
|
|
10
10
|
import useEvent from "react-use-event-hook";
|
|
11
|
-
import type { RevealApi } from "reveal.js";
|
|
12
11
|
|
|
13
12
|
type Props = ICellRendererProps<SlidesLayout>;
|
|
14
13
|
|
|
@@ -26,7 +25,6 @@ export const SlidesLayoutRenderer: React.FC<Props> = ({
|
|
|
26
25
|
const numColumns = useAtomValue(numColumnsAtom);
|
|
27
26
|
const isMultiColumn = numColumns > 1;
|
|
28
27
|
const [activeCellId, setActiveCellId] = useState<CellId | null>(null);
|
|
29
|
-
const deckRef = useRef<RevealApi | null>(null);
|
|
30
28
|
|
|
31
29
|
const { cellsWithOutput, skippedIds, slideTypes, startCellIndex } = useMemo(
|
|
32
30
|
() => computeSlideCellsInfo(cells, layout),
|
|
@@ -53,9 +51,9 @@ export const SlidesLayoutRenderer: React.FC<Props> = ({
|
|
|
53
51
|
setLayout={setLayout}
|
|
54
52
|
activeIndex={resolvedIndex}
|
|
55
53
|
onSlideChange={handleSlideChange}
|
|
56
|
-
|
|
57
|
-
configWidth={250}
|
|
54
|
+
configWidth={300}
|
|
58
55
|
mode={mode}
|
|
56
|
+
isEditable={mode !== "read"}
|
|
59
57
|
/>
|
|
60
58
|
);
|
|
61
59
|
|
|
@@ -34,6 +34,7 @@ import { useResolvedMarimoConfig } from "@/core/config/config";
|
|
|
34
34
|
import { CSSClasses, KnownQueryParams } from "@/core/constants";
|
|
35
35
|
import type { MarimoError, OutputMessage } from "@/core/kernel/messages";
|
|
36
36
|
import { kernelStateAtom } from "@/core/kernel/state";
|
|
37
|
+
import { useNotebookCodeAvailable } from "@/core/meta/code-visibility";
|
|
37
38
|
import { showCodeInRunModeAtom } from "@/core/meta/state";
|
|
38
39
|
import { isErrorMime } from "@/core/mime";
|
|
39
40
|
import { type AppMode, kioskModeAtom } from "@/core/mode";
|
|
@@ -83,23 +84,7 @@ const VerticalLayoutRenderer: React.FC<VerticalLayoutProps> = ({
|
|
|
83
84
|
: showCodeByQueryParam === "true";
|
|
84
85
|
});
|
|
85
86
|
|
|
86
|
-
const
|
|
87
|
-
const cellsHaveCode = cells.some((cell) => Boolean(cell.code));
|
|
88
|
-
|
|
89
|
-
if (kioskMode) {
|
|
90
|
-
return true;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Only show code if in read mode and there is at least one cell with code
|
|
94
|
-
|
|
95
|
-
// If it is a static-notebook or wasm-read-only-notebook, code is always included,
|
|
96
|
-
// but it can be turned it off via a query parameter (include-code=false)
|
|
97
|
-
|
|
98
|
-
const includeCode = urlParams.get(KnownQueryParams.includeCode);
|
|
99
|
-
return mode === "read" && includeCode !== "false" && cellsHaveCode;
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
const canShowCode = evaluateCanShowCode();
|
|
87
|
+
const canShowCode = useNotebookCodeAvailable(cells);
|
|
103
88
|
|
|
104
89
|
const renderCell = (cell: CellRuntimeState & CellData) => {
|
|
105
90
|
return (
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
startTransition,
|
|
4
5
|
useEffect,
|
|
5
6
|
useMemo,
|
|
6
7
|
useRef,
|
|
@@ -8,7 +9,7 @@ import {
|
|
|
8
9
|
Fragment as ReactFragment,
|
|
9
10
|
} from "react";
|
|
10
11
|
import useEvent from "react-use-event-hook";
|
|
11
|
-
import { ExpandIcon, EyeOffIcon } from "lucide-react";
|
|
12
|
+
import { CodeIcon, ExpandIcon, EyeOffIcon } from "lucide-react";
|
|
12
13
|
import { Deck, Fragment, Slide, Stack } from "@revealjs/react";
|
|
13
14
|
import { Slide as CellOutputSlide } from "@/components/slides/slide";
|
|
14
15
|
import { Button } from "@/components/ui/button";
|
|
@@ -34,6 +35,13 @@ import {
|
|
|
34
35
|
DEFAULT_SLIDE_TYPE,
|
|
35
36
|
SlideSidebar,
|
|
36
37
|
} from "./slide-form";
|
|
38
|
+
import {
|
|
39
|
+
SlideCellReadOnlyView,
|
|
40
|
+
SlideCellView,
|
|
41
|
+
} from "@/components/slides/slide-cell-view";
|
|
42
|
+
import { cn } from "@/utils/cn";
|
|
43
|
+
import { isIslands } from "@/core/islands/utils";
|
|
44
|
+
import { useNotebookCodeAvailable } from "@/core/meta/code-visibility";
|
|
37
45
|
import type { AppMode } from "@/core/mode";
|
|
38
46
|
|
|
39
47
|
const ASPECT_RATIO = 16 / 9;
|
|
@@ -118,21 +126,41 @@ function triggerResize(deck: RevealApi | null) {
|
|
|
118
126
|
|
|
119
127
|
const SubslideView = ({
|
|
120
128
|
subslide,
|
|
129
|
+
showCode,
|
|
130
|
+
isEditable,
|
|
121
131
|
}: {
|
|
122
132
|
subslide: ComposedSubslide<RuntimeCell>;
|
|
133
|
+
showCode: boolean;
|
|
134
|
+
isEditable: boolean;
|
|
123
135
|
}) => (
|
|
124
136
|
<Slide>
|
|
125
137
|
<div className="h-full w-full overflow-auto flex">
|
|
126
|
-
<div
|
|
138
|
+
<div
|
|
139
|
+
className={
|
|
140
|
+
showCode ? "mo-slide-content flex flex-col gap-3" : "mo-slide-content"
|
|
141
|
+
}
|
|
142
|
+
style={{
|
|
143
|
+
margin: "auto 20px",
|
|
144
|
+
}}
|
|
145
|
+
>
|
|
127
146
|
{subslide.blocks.map((block, i) => {
|
|
128
|
-
const rendered = block.cells.map((cell) =>
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
147
|
+
const rendered = block.cells.map((cell) => {
|
|
148
|
+
if (!showCode) {
|
|
149
|
+
return (
|
|
150
|
+
<CellOutputSlide
|
|
151
|
+
key={cell.id}
|
|
152
|
+
cellId={cell.id}
|
|
153
|
+
status={cell.status}
|
|
154
|
+
output={cell.output}
|
|
155
|
+
/>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return isEditable ? (
|
|
159
|
+
<SlideCellView key={cell.id} cell={cell} />
|
|
160
|
+
) : (
|
|
161
|
+
<SlideCellReadOnlyView key={cell.id} cell={cell} />
|
|
162
|
+
);
|
|
163
|
+
});
|
|
136
164
|
if (block.isFragment) {
|
|
137
165
|
return (
|
|
138
166
|
<Fragment key={i} as="div">
|
|
@@ -147,27 +175,37 @@ const SubslideView = ({
|
|
|
147
175
|
</Slide>
|
|
148
176
|
);
|
|
149
177
|
|
|
178
|
+
// There is an upstream react bug in dev mode (https://github.com/facebook/react/issues/34840)
|
|
179
|
+
// Uncaught SecurityError: Failed to read a named property '$$typeof' from 'Window'
|
|
180
|
+
// Happens with cells containing iframes / external content
|
|
150
181
|
const RevealSlidesComponent = ({
|
|
151
182
|
cellsWithOutput,
|
|
152
183
|
layout,
|
|
153
184
|
setLayout,
|
|
154
185
|
activeIndex,
|
|
155
186
|
onSlideChange,
|
|
156
|
-
deckRef,
|
|
157
187
|
mode,
|
|
158
188
|
configWidth = 300, // px
|
|
189
|
+
isEditable = false,
|
|
159
190
|
}: {
|
|
160
191
|
cellsWithOutput: RuntimeCell[];
|
|
161
192
|
layout: SlidesLayout;
|
|
162
193
|
setLayout: (layout: SlidesLayout) => void;
|
|
163
194
|
activeIndex?: number;
|
|
164
195
|
onSlideChange?: (index: number) => void;
|
|
165
|
-
deckRef: React.RefObject<RevealApi | null>;
|
|
166
196
|
mode: AppMode;
|
|
167
197
|
configWidth?: number;
|
|
198
|
+
isEditable?: boolean;
|
|
168
199
|
}) => {
|
|
169
200
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
201
|
+
const deckRef = useRef<RevealApi | null>(null);
|
|
170
202
|
const { width, height } = useSlideDimensions(containerRef);
|
|
203
|
+
|
|
204
|
+
const [showCode, setShowCode] = useState(false);
|
|
205
|
+
const codeAvailable = useNotebookCodeAvailable(cellsWithOutput);
|
|
206
|
+
const codeToggleEnabled = !isIslands() && codeAvailable;
|
|
207
|
+
const codeShown = codeToggleEnabled && showCode;
|
|
208
|
+
|
|
171
209
|
const activeCell =
|
|
172
210
|
activeIndex != null ? cellsWithOutput[activeIndex] : undefined;
|
|
173
211
|
// Fall back to the first cell while the deck settles on an initial slide.
|
|
@@ -217,11 +255,7 @@ const RevealSlidesComponent = ({
|
|
|
217
255
|
[width, height, deckTransition],
|
|
218
256
|
);
|
|
219
257
|
|
|
220
|
-
|
|
221
|
-
const deck = deckRef.current;
|
|
222
|
-
if (deck == null) {
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
258
|
+
const navigateDeckToActiveCell = useEvent((deck: RevealApi) => {
|
|
225
259
|
const target = resolveDeckNavigationTarget({
|
|
226
260
|
activeIndex,
|
|
227
261
|
cells: cellsWithOutput,
|
|
@@ -234,7 +268,43 @@ const RevealSlidesComponent = ({
|
|
|
234
268
|
}
|
|
235
269
|
deck.slide(next.h, next.v, next.f);
|
|
236
270
|
clearPreviousVerticalIndices(deck);
|
|
237
|
-
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
useEffect(() => {
|
|
274
|
+
const deck = deckRef.current;
|
|
275
|
+
if (deck == null) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
navigateDeckToActiveCell(deck);
|
|
279
|
+
}, [activeIndex, cellToTarget, cellsWithOutput, navigateDeckToActiveCell]);
|
|
280
|
+
|
|
281
|
+
// Toggling code (re)mounts a CodeMirror editor on the active slide. Defer
|
|
282
|
+
// the state update so the button/keypress paints first and the heavier mount
|
|
283
|
+
// can be interrupted by higher-priority work.
|
|
284
|
+
const toggleShowCode = useEvent(() => {
|
|
285
|
+
startTransition(() => setShowCode((value) => !value));
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const handleDeckReady = useEvent((deck: RevealApi) => {
|
|
289
|
+
navigateDeckToActiveCell(deck);
|
|
290
|
+
if (codeToggleEnabled) {
|
|
291
|
+
deck.addKeyBinding(
|
|
292
|
+
{ keyCode: 67, key: "C", description: "Toggle code editor" },
|
|
293
|
+
toggleShowCode,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const activeSubslide = useMemo(() => {
|
|
299
|
+
if (!activeCell) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
const target = cellToTarget.get(activeCell.id);
|
|
303
|
+
if (!target) {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
return { h: target.h, v: target.v };
|
|
307
|
+
}, [activeCell, cellToTarget]);
|
|
238
308
|
|
|
239
309
|
// Forward the deck's current cell to the parent, except while a skipped
|
|
240
310
|
// preview is parked: every reveal.js event during that window is an echo
|
|
@@ -266,6 +336,7 @@ const RevealSlidesComponent = ({
|
|
|
266
336
|
if (Events.fromInput(event)) {
|
|
267
337
|
return;
|
|
268
338
|
}
|
|
339
|
+
|
|
269
340
|
const direction = classifyNavKey(event);
|
|
270
341
|
if (direction === 0) {
|
|
271
342
|
return;
|
|
@@ -279,6 +350,11 @@ const RevealSlidesComponent = ({
|
|
|
279
350
|
onSlideChange?.(nextIndex);
|
|
280
351
|
});
|
|
281
352
|
|
|
353
|
+
const handleSlideChange = useEvent(() => {
|
|
354
|
+
reportCurrentCell();
|
|
355
|
+
triggerResize(deckRef.current);
|
|
356
|
+
});
|
|
357
|
+
|
|
282
358
|
useEventListener(document, "keydown", handleParkedNavKey, { capture: true });
|
|
283
359
|
|
|
284
360
|
return (
|
|
@@ -292,23 +368,38 @@ const RevealSlidesComponent = ({
|
|
|
292
368
|
deckRef={deckRef}
|
|
293
369
|
className="aspect-video w-full overflow-hidden border rounded bg-background mo-slides-theme prose-slides"
|
|
294
370
|
config={revealConfig}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const deck = deckRef.current;
|
|
298
|
-
triggerResize(deck);
|
|
299
|
-
}}
|
|
371
|
+
onReady={handleDeckReady}
|
|
372
|
+
onSlideChange={handleSlideChange}
|
|
300
373
|
onFragmentShown={reportCurrentCell}
|
|
301
374
|
onFragmentHidden={reportCurrentCell}
|
|
302
375
|
>
|
|
303
|
-
{composition.stacks.map((stack,
|
|
376
|
+
{composition.stacks.map((stack, h) => {
|
|
304
377
|
if (stack.subslides.length === 1) {
|
|
305
|
-
|
|
378
|
+
const isActive =
|
|
379
|
+
activeSubslide?.h === h && activeSubslide?.v === 0;
|
|
380
|
+
return (
|
|
381
|
+
<SubslideView
|
|
382
|
+
key={h}
|
|
383
|
+
subslide={stack.subslides[0]}
|
|
384
|
+
showCode={codeShown && isActive}
|
|
385
|
+
isEditable={isEditable}
|
|
386
|
+
/>
|
|
387
|
+
);
|
|
306
388
|
}
|
|
307
389
|
return (
|
|
308
|
-
<Stack key={
|
|
309
|
-
{stack.subslides.map((sub,
|
|
310
|
-
|
|
311
|
-
|
|
390
|
+
<Stack key={h}>
|
|
391
|
+
{stack.subslides.map((sub, v) => {
|
|
392
|
+
const isActive =
|
|
393
|
+
activeSubslide?.h === h && activeSubslide?.v === v;
|
|
394
|
+
return (
|
|
395
|
+
<SubslideView
|
|
396
|
+
key={v}
|
|
397
|
+
subslide={sub}
|
|
398
|
+
showCode={codeShown && isActive}
|
|
399
|
+
isEditable={isEditable}
|
|
400
|
+
/>
|
|
401
|
+
);
|
|
402
|
+
})}
|
|
312
403
|
</Stack>
|
|
313
404
|
);
|
|
314
405
|
})}
|
|
@@ -336,24 +427,45 @@ const RevealSlidesComponent = ({
|
|
|
336
427
|
</div>
|
|
337
428
|
</div>
|
|
338
429
|
)}
|
|
339
|
-
<
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
430
|
+
<div className="absolute top-2 right-2 z-20 opacity-0 group-hover:opacity-70 text-muted-foreground transition-opacity">
|
|
431
|
+
{codeToggleEnabled && (
|
|
432
|
+
<Tooltip content={codeShown ? "Hide code (C)" : "Show code (C)"}>
|
|
433
|
+
<Button
|
|
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)">
|
|
450
|
+
<Button
|
|
451
|
+
data-testid="marimo-plugin-slides-fullscreen"
|
|
452
|
+
variant="ghost"
|
|
453
|
+
size="icon"
|
|
454
|
+
className="text-muted-foreground h-7 w-7"
|
|
455
|
+
aria-label="Enter fullscreen"
|
|
456
|
+
onClick={() => {
|
|
457
|
+
deckRef.current
|
|
458
|
+
?.getViewportElement()
|
|
459
|
+
?.requestFullscreen()
|
|
460
|
+
.catch((error) => {
|
|
461
|
+
Logger.error("Failed to request fullscreen", error);
|
|
462
|
+
});
|
|
463
|
+
}}
|
|
464
|
+
>
|
|
465
|
+
<ExpandIcon className="h-4 w-4" />
|
|
466
|
+
</Button>
|
|
467
|
+
</Tooltip>
|
|
468
|
+
</div>
|
|
357
469
|
</div>
|
|
358
470
|
</div>
|
|
359
471
|
|