@marimo-team/frontend 0.23.7-dev64 → 0.23.7-dev66
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/{dependency-graph-panel-BjOeXp74.js → dependency-graph-panel-69vKwEHr.js} +3 -3
- package/dist/assets/{edit-page-Dos8zz_9.js → edit-page-DAtUxLge.js} +4 -4
- package/dist/assets/{hooks-7OHHugrQ.js → hooks-DZNVrVar.js} +1 -1
- package/dist/assets/index-DH00kbhV.css +2 -0
- package/dist/assets/{index-1EIgCVR_.js → index-Ds8jJqO4.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/dependency-graph/minimap-content.tsx +14 -3
- 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,127 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { StickyNoteIcon } from "lucide-react";
|
|
4
|
+
import { useEffect, useRef, useState } from "react";
|
|
5
|
+
import useEvent from "react-use-event-hook";
|
|
6
|
+
import type { CellId } from "@/core/cells/ids";
|
|
7
|
+
import { useDebouncedCallback } from "@/hooks/useDebounce";
|
|
8
|
+
import { cn } from "@/utils/cn";
|
|
9
|
+
import { Events } from "@/utils/events";
|
|
10
|
+
import type { SlidesLayout } from "../editor/renderers/slides-layout/types";
|
|
11
|
+
|
|
12
|
+
interface SlideNotesEditorProps {
|
|
13
|
+
layout: SlidesLayout;
|
|
14
|
+
setLayout: (layout: SlidesLayout) => void;
|
|
15
|
+
cellId: CellId | undefined;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const PERSIST_DELAY_MS = 300;
|
|
20
|
+
|
|
21
|
+
export const SlideNotesEditor = ({
|
|
22
|
+
layout,
|
|
23
|
+
setLayout,
|
|
24
|
+
cellId,
|
|
25
|
+
className,
|
|
26
|
+
}: SlideNotesEditorProps) => {
|
|
27
|
+
const initialValue = cellId
|
|
28
|
+
? (layout.cells.get(cellId)?.speakerNotes ?? "")
|
|
29
|
+
: "";
|
|
30
|
+
|
|
31
|
+
const [draft, setDraft] = useState(initialValue);
|
|
32
|
+
|
|
33
|
+
// Tracks whether the user has typed something that hasn't been persisted
|
|
34
|
+
// yet. Used to decide if the textarea is safe to overwrite from props.
|
|
35
|
+
const hasPendingEditRef = useRef(false);
|
|
36
|
+
|
|
37
|
+
// The debounced callback takes `(cellId, text)` so a `flush()` replays with
|
|
38
|
+
// the latest args — which means the in-flight text lands on the slide it
|
|
39
|
+
// was typed for, even if `cellId` has since changed.
|
|
40
|
+
const persistImmediate = useEvent((targetCellId: CellId, next: string) => {
|
|
41
|
+
hasPendingEditRef.current = false;
|
|
42
|
+
const existing = layout.cells.get(targetCellId);
|
|
43
|
+
if ((existing?.speakerNotes ?? "") === next) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const newCells = new Map(layout.cells);
|
|
47
|
+
newCells.set(targetCellId, { ...existing, speakerNotes: next });
|
|
48
|
+
setLayout({ ...layout, cells: newCells });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const persistDebounced = useDebouncedCallback(
|
|
52
|
+
persistImmediate,
|
|
53
|
+
PERSIST_DELAY_MS,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// Keep the textarea in sync with `layout`:
|
|
57
|
+
// - On slide switch, flush any in-flight edit to the *previous* slide before
|
|
58
|
+
// adopting the new slide's notes.
|
|
59
|
+
// - On same-slide updates (e.g. future undo/redo or external setLayout
|
|
60
|
+
// writers), adopt the new value only when the user isn't mid-edit so
|
|
61
|
+
// pending keystrokes aren't clobbered.
|
|
62
|
+
const prevCellIdRef = useRef(cellId);
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (prevCellIdRef.current !== cellId) {
|
|
65
|
+
persistDebounced.flush();
|
|
66
|
+
hasPendingEditRef.current = false;
|
|
67
|
+
setDraft(initialValue);
|
|
68
|
+
prevCellIdRef.current = cellId;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (!hasPendingEditRef.current && initialValue !== draft) {
|
|
72
|
+
setDraft(initialValue);
|
|
73
|
+
}
|
|
74
|
+
}, [cellId, initialValue, draft, persistDebounced]);
|
|
75
|
+
|
|
76
|
+
// Flush on unmount so closing the panel / navigating away doesn't lose text.
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
return () => {
|
|
79
|
+
persistDebounced.flush();
|
|
80
|
+
};
|
|
81
|
+
}, [persistDebounced]);
|
|
82
|
+
|
|
83
|
+
const handleChange = (next: string) => {
|
|
84
|
+
setDraft(next);
|
|
85
|
+
if (cellId) {
|
|
86
|
+
hasPendingEditRef.current = true;
|
|
87
|
+
persistDebounced(cellId, next);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<section
|
|
93
|
+
className={cn(
|
|
94
|
+
"h-full min-h-0 flex flex-col bg-muted/40 dark:bg-muted/20 border-t",
|
|
95
|
+
className,
|
|
96
|
+
)}
|
|
97
|
+
aria-label="Speaker notes"
|
|
98
|
+
// Keep keystrokes inside the textarea from advancing the reveal.js deck.
|
|
99
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
100
|
+
>
|
|
101
|
+
<header className="flex items-center gap-1.5 px-3 h-8 shrink-0 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
102
|
+
<StickyNoteIcon className="h-3.5 w-3.5" />
|
|
103
|
+
<span>Speaker notes</span>
|
|
104
|
+
</header>
|
|
105
|
+
<div className="flex-1 min-h-0 p-2">
|
|
106
|
+
{cellId ? (
|
|
107
|
+
<textarea
|
|
108
|
+
value={draft}
|
|
109
|
+
onChange={(event) => handleChange(event.target.value)}
|
|
110
|
+
onClick={Events.stopPropagation()}
|
|
111
|
+
placeholder="Add notes for this slide. Visible to you in speaker view (press S during presentation)."
|
|
112
|
+
className={cn(
|
|
113
|
+
"h-full w-full resize-none rounded-sm border border-input/25 bg-background",
|
|
114
|
+
"px-3 py-2 text-sm leading-relaxed text-foreground placeholder:text-muted-foreground",
|
|
115
|
+
"ring-offset-background focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring focus-visible:border-accent",
|
|
116
|
+
)}
|
|
117
|
+
aria-label="Speaker notes for the current slide"
|
|
118
|
+
/>
|
|
119
|
+
) : (
|
|
120
|
+
<div className="h-full flex items-center justify-center text-xs text-muted-foreground">
|
|
121
|
+
Select a slide to add notes.
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
</section>
|
|
126
|
+
);
|
|
127
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import type { CellId } from "@/core/cells/ids";
|
|
4
|
+
import type { SlideConfig } from "../editor/renderers/slides-layout/types";
|
|
5
|
+
import type { ComposedSubslide } from "./compose-slides";
|
|
6
|
+
|
|
7
|
+
/** Lone-line marker between blocks; `<NotesAside>` renders this as `<hr>`. */
|
|
8
|
+
export const NOTES_DIVIDER = "---";
|
|
9
|
+
|
|
10
|
+
const BLOCK_JOIN = `\n\n${NOTES_DIVIDER}\n\n`;
|
|
11
|
+
|
|
12
|
+
export const collectBlockNotes = <C extends { id: CellId }>(
|
|
13
|
+
cells: readonly C[],
|
|
14
|
+
slideConfigs: ReadonlyMap<CellId, SlideConfig>,
|
|
15
|
+
): string =>
|
|
16
|
+
cells
|
|
17
|
+
.map((cell) => slideConfigs.get(cell.id)?.speakerNotes ?? "")
|
|
18
|
+
.filter((note) => note.trim().length > 0)
|
|
19
|
+
.join("\n\n");
|
|
20
|
+
|
|
21
|
+
export interface SubslideNotes {
|
|
22
|
+
/** Notes shown when no fragment is current. */
|
|
23
|
+
slideLevel: string;
|
|
24
|
+
/** Cumulative notes for each fragment block, keyed by block index. */
|
|
25
|
+
cumulativeByBlock: Map<number, string>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Per-fragment cumulative notes: slide-level notes plus each revealed
|
|
30
|
+
* fragment's notes, separated by a horizontal-rule line.
|
|
31
|
+
*/
|
|
32
|
+
export const buildSubslideNotes = <C extends { id: CellId }>(
|
|
33
|
+
subslide: ComposedSubslide<C>,
|
|
34
|
+
slideConfigs: ReadonlyMap<CellId, SlideConfig>,
|
|
35
|
+
): SubslideNotes => {
|
|
36
|
+
const blockNotes = subslide.blocks.map((block) =>
|
|
37
|
+
collectBlockNotes(block.cells, slideConfigs),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const slideLevel = subslide.blocks
|
|
41
|
+
.map((block, i) => (block.isFragment ? "" : blockNotes[i]))
|
|
42
|
+
.filter((note) => note.length > 0)
|
|
43
|
+
.join("\n\n");
|
|
44
|
+
|
|
45
|
+
const cumulativeByBlock = new Map<number, string>();
|
|
46
|
+
const revealsSoFar: string[] = [];
|
|
47
|
+
subslide.blocks.forEach((block, i) => {
|
|
48
|
+
if (!block.isFragment) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const myNotes = blockNotes[i];
|
|
52
|
+
if (myNotes.length > 0) {
|
|
53
|
+
revealsSoFar.push(myNotes);
|
|
54
|
+
}
|
|
55
|
+
const accumulated = [slideLevel, ...revealsSoFar]
|
|
56
|
+
.filter((s) => s.length > 0)
|
|
57
|
+
.join(BLOCK_JOIN);
|
|
58
|
+
if (accumulated.length > 0) {
|
|
59
|
+
cumulativeByBlock.set(i, accumulated);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return { slideLevel, cumulativeByBlock };
|
|
64
|
+
};
|
|
@@ -31,6 +31,20 @@
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/* Resize handle for the notes panel below the deck. Self-contained so the
|
|
35
|
+
slides layout doesn't have to inherit styles from app-chrome.css. */
|
|
36
|
+
.mo-slides-notes-resize {
|
|
37
|
+
height: 4px;
|
|
38
|
+
background-color: transparent;
|
|
39
|
+
transition: background-color 200ms linear;
|
|
40
|
+
outline: none;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.mo-slides-notes-resize:hover,
|
|
44
|
+
.mo-slides-notes-resize[data-resize-handle-active] {
|
|
45
|
+
background-color: var(--slate-11);
|
|
46
|
+
}
|
|
47
|
+
|
|
34
48
|
/* helpful for debugging */
|
|
35
49
|
/* .mo-slide-content {
|
|
36
50
|
background-color: red;
|
package/src/core/edit-app.tsx
CHANGED
|
@@ -157,7 +157,7 @@ export const EditApp: React.FC<AppProps> = ({
|
|
|
157
157
|
"sticky left-0",
|
|
158
158
|
)}
|
|
159
159
|
>
|
|
160
|
-
{isEditing && (
|
|
160
|
+
{!hideControls && isEditing && (
|
|
161
161
|
<div className="flex items-center justify-center container">
|
|
162
162
|
<FilenameForm filename={filename} />
|
|
163
163
|
</div>
|