@marimo-team/frontend 0.23.1-dev7 → 0.23.1-dev8
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/{CellStatus-1vKptZLk.js → CellStatus-zTcdYfqx.js} +1 -1
- package/dist/assets/{JsonOutput-BsAmBgmA.js → JsonOutput-BY31ccA7.js} +1 -1
- package/dist/assets/{MarimoErrorOutput-BrEeS6om.js → MarimoErrorOutput--Yd2Aw0J.js} +1 -1
- package/dist/assets/{RenderHTML-DvfCr2Ox.js → RenderHTML-CbuarQqA.js} +1 -1
- package/dist/assets/{add-cell-with-ai-Dt7oHdbm.js → add-cell-with-ai-_Y6SqxBB.js} +1 -1
- package/dist/assets/{add-connection-dialog-CspEHMf4.js → add-connection-dialog-CjvNOKgb.js} +1 -1
- package/dist/assets/{agent-panel-BnU7gQWx.js → agent-panel-C24uwabG.js} +1 -1
- package/dist/assets/{ai-model-dropdown-Xik-y-L5.js → ai-model-dropdown-Dyxi3_nW.js} +1 -1
- package/dist/assets/{app-config-button-CHBHFhjc.js → app-config-button-BT2Do4RJ.js} +1 -1
- package/dist/assets/{cell-editor-D_3l2zTD.js → cell-editor-zW0u82sK.js} +1 -1
- package/dist/assets/{cell-link-DJVe6_Zu.js → cell-link-CRkrHl-y.js} +1 -1
- package/dist/assets/{cells-S7rO4svP.js → cells-BqYYXi6G.js} +69 -69
- package/dist/assets/{chat-display-CfEsEqVW.js → chat-display-DsHMZa9F.js} +1 -1
- package/dist/assets/{chat-panel-qK2fGD8c.js → chat-panel-o9D3upnX.js} +1 -1
- package/dist/assets/{chat-ui-D0Zk2tGi.js → chat-ui-BYS03y86.js} +1 -1
- package/dist/assets/{column-preview-BZ6dVJcf.js → column-preview-Dwv5a_zE.js} +1 -1
- package/dist/assets/{command-palette-1FgTXBti.js → command-palette-BYbKGSF3.js} +1 -1
- package/dist/assets/{common-BmgcLq5w.js → common-DeoGL9rK.js} +1 -1
- package/dist/assets/{components-B_dPGsbP.js → components-CDgxb-5o.js} +1 -1
- package/dist/assets/{components-D2yZZbqm.js → components-DKHyHZBv.js} +1 -1
- package/dist/assets/{datasource-t6MwjjVj.js → datasource-COFRe84u.js} +1 -1
- package/dist/assets/{dependency-graph-panel-BadtKupA.js → dependency-graph-panel-BXSe6z1R.js} +1 -1
- package/dist/assets/{documentation-panel-BsLlmX7w.js → documentation-panel-CA2pWMgB.js} +1 -1
- package/dist/assets/{download-Do1WPYs4.js → download-5XbM3TL_.js} +1 -1
- package/dist/assets/edit-page-CMUN3ESy.js +9 -0
- package/dist/assets/{error-panel-CBVjdcTs.js → error-panel-CbqfK1HJ.js} +1 -1
- package/dist/assets/{file-explorer-panel-DYR37L0M.js → file-explorer-panel-CbS8z-JR.js} +1 -1
- package/dist/assets/{file-icons-Ce885dch.js → file-icons-Bj5YoM7H.js} +1 -1
- package/dist/assets/{floating-outline-x0sdO8LG.js → floating-outline-XObNWtN8.js} +1 -1
- package/dist/assets/{focus-DTtb8f52.js → focus-DzMo6UAI.js} +1 -1
- package/dist/assets/{form-D_Nha4Lp.js → form-DLyXacSF.js} +1 -1
- package/dist/assets/{home-page-CoJ_ZMWR.js → home-page-BUdd5uTz.js} +1 -1
- package/dist/assets/{hooks-Cx6iKOXA.js → hooks-kZJc1iBf.js} +1 -1
- package/dist/assets/{html-to-image-B2vXpMPW.js → html-to-image-DGqJ93hW.js} +1 -1
- package/dist/assets/index-CKRn_SiB.css +2 -0
- package/dist/assets/{index-B_D5e64b.js → index-bjxpaV0V.js} +5 -5
- package/dist/assets/{kiosk-mode-FcVQMZAH.js → kiosk-mode-DYHoqMaZ.js} +1 -1
- package/dist/assets/layout-tmN-U1zs.js +9 -0
- package/dist/assets/{logs-panel-Dsopo0A4.js → logs-panel-CRW4c2IL.js} +1 -1
- package/dist/assets/{markdown-renderer-Ds5PRrQP.js → markdown-renderer-DNANigO8.js} +1 -1
- package/dist/assets/{name-cell-input-CYTm4rHn.js → name-cell-input-3iKP6YTw.js} +1 -1
- package/dist/assets/{outline-panel-C6Gebwlt.js → outline-panel-VIqWcHj6.js} +1 -1
- package/dist/assets/{packages-panel-Cx5Im5-h.js → packages-panel-D_z4ylBE.js} +1 -1
- package/dist/assets/panels-CLfdzLPR.js +1 -0
- package/dist/assets/{process-output-DxNLeVL1.js → process-output-Q6wVr7a-.js} +1 -1
- package/dist/assets/{readonly-python-code-CB7U_Wc5.js → readonly-python-code-CI_b818F.js} +1 -1
- package/dist/assets/{run-page-CGoGL9nm.js → run-page-DPuH6QY4.js} +1 -1
- package/dist/assets/{scratchpad-panel-DR4mmtqX.js → scratchpad-panel-BsMm0GQP.js} +1 -1
- package/dist/assets/{session-panel-DGqZrbYK.js → session-panel-CTDzGShO.js} +1 -1
- package/dist/assets/{slides-component-BIXn0Nqk.js → slides-component-ncUJNz7U.js} +1 -1
- package/dist/assets/{snippets-panel-CU_AkTo5.js → snippets-panel-CWof0wHk.js} +1 -1
- package/dist/assets/{state-Di6_R3-d.js → state-BvnlMKdT.js} +1 -1
- package/dist/assets/{state-iGDxMYGl.js → state-DPomuurt.js} +1 -1
- package/dist/assets/{textarea-cV4DzEoq.js → textarea-CS2o3y4W.js} +1 -1
- package/dist/assets/{tracing-6MHdsIto.js → tracing-CPDDwzIA.js} +1 -1
- package/dist/assets/{tracing-panel-Cwuf0kYN.js → tracing-panel-Ku1LapXJ.js} +2 -2
- package/dist/assets/{useAddCell-DDDgUZhC.js → useAddCell-B6yUY_RG.js} +1 -1
- package/dist/assets/{useCellActionButton-BceYv-6H.js → useCellActionButton-SxeK4dmW.js} +1 -1
- package/dist/assets/{useDeleteCell-CwBNr3-p.js → useDeleteCell-DHUjJQJx.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-Bjv6Z79M.js → useDependencyPanelTab-CflgayoH.js} +1 -1
- package/dist/assets/{useNotebookActions-0DS32qpY.js → useNotebookActions-DHBEqrc_.js} +1 -1
- package/dist/assets/{useRunCells-Bf82xWy5.js → useRunCells-DFYAOTWd.js} +1 -1
- package/dist/assets/{useSplitCell-WZ71D3bV.js → useSplitCell-Bh-NZsBl.js} +1 -1
- package/dist/index.html +23 -23
- package/package.json +1 -1
- package/src/components/editor/renderers/slides-layout/slides-layout.tsx +50 -44
- package/src/components/slides/__tests__/minimap.test.ts +402 -0
- package/src/components/slides/minimap.tsx +534 -0
- package/src/components/slides/slide.tsx +29 -0
- package/src/components/slides/slides-component.tsx +16 -1
- package/src/core/cells/__tests__/cells.test.ts +105 -1
- package/src/core/cells/cells.ts +43 -0
- package/src/core/cells/document-changes.ts +2 -1
- package/dist/assets/edit-page-CSyxrzTp.js +0 -13
- package/dist/assets/index-qO0a4zuT.css +0 -2
- package/dist/assets/layout-DW9T7Upe.js +0 -5
- package/dist/assets/panels-CDCHQBRn.js +0 -1
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { useCellActions, useCellIds } from "@/core/cells/cells";
|
|
4
|
+
import type { CellId } from "@/core/cells/ids";
|
|
5
|
+
import type { CellColumnId } from "@/utils/id-tree";
|
|
6
|
+
import { useEffect, useRef, useState } from "react";
|
|
7
|
+
import type { ICellRendererProps } from "../editor/renderers/types";
|
|
8
|
+
import type { SlidesLayout } from "../editor/renderers/slides-layout/types";
|
|
9
|
+
import {
|
|
10
|
+
DndContext,
|
|
11
|
+
DragOverlay,
|
|
12
|
+
PointerSensor,
|
|
13
|
+
useSensor,
|
|
14
|
+
useSensors,
|
|
15
|
+
type DragMoveEvent,
|
|
16
|
+
type DragOverEvent,
|
|
17
|
+
type DragStartEvent,
|
|
18
|
+
type DragEndEvent,
|
|
19
|
+
closestCenter,
|
|
20
|
+
pointerWithin,
|
|
21
|
+
type CollisionDetection,
|
|
22
|
+
type UniqueIdentifier,
|
|
23
|
+
} from "@dnd-kit/core";
|
|
24
|
+
import {
|
|
25
|
+
SortableContext,
|
|
26
|
+
useSortable,
|
|
27
|
+
verticalListSortingStrategy,
|
|
28
|
+
} from "@dnd-kit/sortable";
|
|
29
|
+
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
|
|
30
|
+
import { cn } from "@/utils/cn";
|
|
31
|
+
import { Slide } from "./slide";
|
|
32
|
+
import { InfoIcon } from "lucide-react";
|
|
33
|
+
import { Logger } from "@/utils/Logger";
|
|
34
|
+
|
|
35
|
+
type Props = ICellRendererProps<SlidesLayout>;
|
|
36
|
+
type SlideCell = Props["cells"][number];
|
|
37
|
+
type CellIdsState = ReturnType<typeof useCellIds>;
|
|
38
|
+
type DropPosition = "before" | "after";
|
|
39
|
+
|
|
40
|
+
interface ProjectedDropTarget {
|
|
41
|
+
overId: CellId;
|
|
42
|
+
position: DropPosition;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface ResolvedDropTarget {
|
|
46
|
+
cellId: CellId;
|
|
47
|
+
columnId: CellColumnId;
|
|
48
|
+
index: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface SlideThumbnailCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
52
|
+
cell: SlideCell;
|
|
53
|
+
isActiveSlide?: boolean;
|
|
54
|
+
isActiveDragSource?: boolean;
|
|
55
|
+
isOverlay?: boolean;
|
|
56
|
+
isVisible?: boolean;
|
|
57
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface SlideThumbnailRowProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
61
|
+
cell: SlideCell;
|
|
62
|
+
isActiveSlide?: boolean;
|
|
63
|
+
dropIndicator?: DropPosition | null;
|
|
64
|
+
isActiveDragSource?: boolean;
|
|
65
|
+
isVisible?: boolean;
|
|
66
|
+
ref?: React.Ref<HTMLButtonElement>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface SlidesMinimapProps {
|
|
70
|
+
cells: SlideCell[];
|
|
71
|
+
canReorder: boolean;
|
|
72
|
+
activeCellId: CellId | null;
|
|
73
|
+
onSlideClick: (index: number) => void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const THUMBNAIL_WIDTH = 256;
|
|
77
|
+
const THUMBNAIL_HEIGHT = 144;
|
|
78
|
+
const THUMBNAIL_SCALE = 0.3;
|
|
79
|
+
const MINIMAP_AUTO_SCROLL = {
|
|
80
|
+
threshold: { x: 0, y: 0.1 },
|
|
81
|
+
};
|
|
82
|
+
const MINIMAP_GAP = 8;
|
|
83
|
+
const VISIBILITY_ROOT_MARGIN = "200px 0px";
|
|
84
|
+
const minimapCollisionDetection: CollisionDetection = (args) => {
|
|
85
|
+
const pointerCollisions = pointerWithin(args);
|
|
86
|
+
return pointerCollisions.length > 0 ? pointerCollisions : closestCenter(args);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Tracks which `[data-cell-id]` elements inside a scrollable container are
|
|
91
|
+
* within (or near) the viewport using a single shared IntersectionObserver.
|
|
92
|
+
* A MutationObserver re-observes when children are added or removed.
|
|
93
|
+
* Off-screen thumbnails can skip rendering expensive <Slide> content.
|
|
94
|
+
*/
|
|
95
|
+
function useVisibleCellIds(
|
|
96
|
+
containerRef: React.RefObject<HTMLDivElement | null>,
|
|
97
|
+
): ReadonlySet<CellId> {
|
|
98
|
+
const [visibleIds, setVisibleIds] = useState<ReadonlySet<CellId>>(
|
|
99
|
+
() => new Set(),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
const container = containerRef.current;
|
|
104
|
+
if (!container) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const intersectionObserver = new IntersectionObserver(
|
|
109
|
+
(entries) => {
|
|
110
|
+
setVisibleIds((prev) => {
|
|
111
|
+
const next = new Set(prev);
|
|
112
|
+
let changed = false;
|
|
113
|
+
for (const entry of entries) {
|
|
114
|
+
const id = (entry.target as HTMLElement).dataset.cellId as
|
|
115
|
+
| CellId
|
|
116
|
+
| undefined;
|
|
117
|
+
if (!id) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (entry.isIntersecting && !next.has(id)) {
|
|
121
|
+
next.add(id);
|
|
122
|
+
changed = true;
|
|
123
|
+
} else if (!entry.isIntersecting && next.has(id)) {
|
|
124
|
+
next.delete(id);
|
|
125
|
+
changed = true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return changed ? next : prev;
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
{ root: container, rootMargin: VISIBILITY_ROOT_MARGIN },
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const observeAll = () => {
|
|
135
|
+
intersectionObserver.disconnect();
|
|
136
|
+
for (const el of container.querySelectorAll<HTMLElement>(
|
|
137
|
+
"[data-cell-id]",
|
|
138
|
+
)) {
|
|
139
|
+
intersectionObserver.observe(el);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
observeAll();
|
|
144
|
+
|
|
145
|
+
const mutationObserver = new MutationObserver(observeAll);
|
|
146
|
+
mutationObserver.observe(container, { childList: true });
|
|
147
|
+
|
|
148
|
+
return () => {
|
|
149
|
+
intersectionObserver.disconnect();
|
|
150
|
+
mutationObserver.disconnect();
|
|
151
|
+
};
|
|
152
|
+
}, [containerRef]);
|
|
153
|
+
|
|
154
|
+
return visibleIds;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export const SlidesMinimap = ({
|
|
158
|
+
cells,
|
|
159
|
+
canReorder,
|
|
160
|
+
activeCellId,
|
|
161
|
+
onSlideClick,
|
|
162
|
+
}: SlidesMinimapProps) => {
|
|
163
|
+
const cellIds = useCellIds();
|
|
164
|
+
const { moveCellToIndex } = useCellActions();
|
|
165
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
166
|
+
const visibleIds = useVisibleCellIds(containerRef);
|
|
167
|
+
const [activeId, setActiveId] = useState<CellId | null>(null);
|
|
168
|
+
const [dropTarget, setDropTarget] = useState<ProjectedDropTarget | null>(
|
|
169
|
+
null,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
if (!activeCellId || !containerRef.current) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const el = containerRef.current.querySelector(
|
|
177
|
+
`[data-cell-id="${activeCellId}"]`,
|
|
178
|
+
);
|
|
179
|
+
el?.scrollIntoView({ block: "nearest", behavior: "instant" });
|
|
180
|
+
}, [activeCellId]);
|
|
181
|
+
|
|
182
|
+
const sensors = useSensors(
|
|
183
|
+
useSensor(PointerSensor, {
|
|
184
|
+
activationConstraint: {
|
|
185
|
+
distance: 8,
|
|
186
|
+
},
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const activeCell = activeId
|
|
191
|
+
? (cells.find((cell) => cell.id === activeId) ?? null)
|
|
192
|
+
: null;
|
|
193
|
+
|
|
194
|
+
const resetDragState = () => {
|
|
195
|
+
setActiveId(null);
|
|
196
|
+
setDropTarget(null);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const updateDropTarget = (event: DragMoveEvent | DragOverEvent) => {
|
|
200
|
+
const next = projectDropTarget(event);
|
|
201
|
+
|
|
202
|
+
setDropTarget((prev) => {
|
|
203
|
+
if (prev?.overId === next?.overId && prev?.position === next?.position) {
|
|
204
|
+
return prev;
|
|
205
|
+
}
|
|
206
|
+
return next;
|
|
207
|
+
});
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const handleDragStart = (event: DragStartEvent) => {
|
|
211
|
+
setDropTarget(null);
|
|
212
|
+
const cellId = asCellId(event.active.id);
|
|
213
|
+
if (cellId) {
|
|
214
|
+
setActiveId(cellId);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const handleDragEnd = (_event: DragEndEvent) => {
|
|
219
|
+
try {
|
|
220
|
+
if (activeId && dropTarget) {
|
|
221
|
+
const resolvedTarget = resolveDropTarget({
|
|
222
|
+
cellIds,
|
|
223
|
+
activeId,
|
|
224
|
+
target: dropTarget,
|
|
225
|
+
});
|
|
226
|
+
if (resolvedTarget) {
|
|
227
|
+
moveCellToIndex(resolvedTarget);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
} catch (e) {
|
|
231
|
+
Logger.warn("Drop failed", e);
|
|
232
|
+
} finally {
|
|
233
|
+
resetDragState();
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
if (!canReorder) {
|
|
238
|
+
return (
|
|
239
|
+
<SlideThumbnailsContainer ref={containerRef}>
|
|
240
|
+
<div className="text-xs text-gray-500 flex items-center gap-0.5">
|
|
241
|
+
<InfoIcon className="h-3 w-3" />
|
|
242
|
+
Reordering is not supported in multi-column mode
|
|
243
|
+
</div>
|
|
244
|
+
{cells.map((cell, index) => (
|
|
245
|
+
<SlideThumbnailRow
|
|
246
|
+
key={cell.id}
|
|
247
|
+
cell={cell}
|
|
248
|
+
isActiveSlide={cell.id === activeCellId}
|
|
249
|
+
isVisible={visibleIds.has(cell.id)}
|
|
250
|
+
onClick={() => onSlideClick(index)}
|
|
251
|
+
/>
|
|
252
|
+
))}
|
|
253
|
+
</SlideThumbnailsContainer>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<DndContext
|
|
259
|
+
autoScroll={MINIMAP_AUTO_SCROLL}
|
|
260
|
+
collisionDetection={minimapCollisionDetection}
|
|
261
|
+
modifiers={[restrictToVerticalAxis]}
|
|
262
|
+
sensors={sensors}
|
|
263
|
+
onDragStart={handleDragStart}
|
|
264
|
+
onDragMove={updateDropTarget}
|
|
265
|
+
onDragOver={updateDropTarget}
|
|
266
|
+
onDragEnd={handleDragEnd}
|
|
267
|
+
onDragCancel={resetDragState}
|
|
268
|
+
>
|
|
269
|
+
<SlideThumbnailsContainer ref={containerRef}>
|
|
270
|
+
<SortableContext
|
|
271
|
+
items={cells.map((cell) => cell.id)}
|
|
272
|
+
strategy={verticalListSortingStrategy}
|
|
273
|
+
>
|
|
274
|
+
{cells.map((cell, index) => (
|
|
275
|
+
<SortableSlideThumbnail
|
|
276
|
+
key={cell.id}
|
|
277
|
+
cell={cell}
|
|
278
|
+
isActive={activeId === cell.id}
|
|
279
|
+
isActiveSlide={cell.id === activeCellId}
|
|
280
|
+
isVisible={visibleIds.has(cell.id)}
|
|
281
|
+
dropIndicator={
|
|
282
|
+
dropTarget?.overId === cell.id && activeId !== cell.id
|
|
283
|
+
? dropTarget.position
|
|
284
|
+
: null
|
|
285
|
+
}
|
|
286
|
+
onClick={() => onSlideClick(index)}
|
|
287
|
+
/>
|
|
288
|
+
))}
|
|
289
|
+
</SortableContext>
|
|
290
|
+
</SlideThumbnailsContainer>
|
|
291
|
+
<DragOverlay>
|
|
292
|
+
{activeCell && (
|
|
293
|
+
<SlideThumbnailCard
|
|
294
|
+
cell={activeCell}
|
|
295
|
+
isOverlay={true}
|
|
296
|
+
isActiveDragSource={true}
|
|
297
|
+
/>
|
|
298
|
+
)}
|
|
299
|
+
</DragOverlay>
|
|
300
|
+
</DndContext>
|
|
301
|
+
);
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const SlideThumbnailsContainer = ({
|
|
305
|
+
children,
|
|
306
|
+
ref,
|
|
307
|
+
}: {
|
|
308
|
+
children: React.ReactNode;
|
|
309
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
310
|
+
}) => {
|
|
311
|
+
return (
|
|
312
|
+
<div
|
|
313
|
+
ref={ref}
|
|
314
|
+
className="h-full overflow-auto flex flex-col scrollbar-thin"
|
|
315
|
+
>
|
|
316
|
+
{children}
|
|
317
|
+
</div>
|
|
318
|
+
);
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
interface SortableSlideThumbnailProps {
|
|
322
|
+
cell: SlideCell;
|
|
323
|
+
dropIndicator?: DropPosition | null;
|
|
324
|
+
isActive: boolean;
|
|
325
|
+
isActiveSlide?: boolean;
|
|
326
|
+
isVisible?: boolean;
|
|
327
|
+
onClick?: () => void;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const SortableSlideThumbnail = ({
|
|
331
|
+
cell,
|
|
332
|
+
dropIndicator,
|
|
333
|
+
isActive,
|
|
334
|
+
isActiveSlide,
|
|
335
|
+
isVisible,
|
|
336
|
+
onClick,
|
|
337
|
+
}: SortableSlideThumbnailProps) => {
|
|
338
|
+
const { attributes, listeners, setNodeRef } = useSortable({
|
|
339
|
+
id: cell.id,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
return (
|
|
343
|
+
<SlideThumbnailRow
|
|
344
|
+
ref={setNodeRef}
|
|
345
|
+
cell={cell}
|
|
346
|
+
dropIndicator={dropIndicator}
|
|
347
|
+
isActiveDragSource={isActive}
|
|
348
|
+
isActiveSlide={isActiveSlide}
|
|
349
|
+
isVisible={isVisible}
|
|
350
|
+
onClick={onClick}
|
|
351
|
+
{...attributes}
|
|
352
|
+
{...listeners}
|
|
353
|
+
/>
|
|
354
|
+
);
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const SlideThumbnailRow = ({
|
|
358
|
+
cell,
|
|
359
|
+
className,
|
|
360
|
+
style,
|
|
361
|
+
dropIndicator,
|
|
362
|
+
isActiveSlide = false,
|
|
363
|
+
isActiveDragSource = false,
|
|
364
|
+
isVisible,
|
|
365
|
+
onClick,
|
|
366
|
+
ref,
|
|
367
|
+
...props
|
|
368
|
+
}: SlideThumbnailRowProps) => {
|
|
369
|
+
const rowStyle: React.CSSProperties = {
|
|
370
|
+
paddingTop: MINIMAP_GAP,
|
|
371
|
+
paddingBottom: MINIMAP_GAP,
|
|
372
|
+
...style,
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
return (
|
|
376
|
+
<button
|
|
377
|
+
ref={ref}
|
|
378
|
+
type="button"
|
|
379
|
+
data-cell-id={cell.id}
|
|
380
|
+
className={cn(
|
|
381
|
+
"relative shrink-0 appearance-none text-left p-0 bg-transparent outline-none",
|
|
382
|
+
className,
|
|
383
|
+
)}
|
|
384
|
+
style={rowStyle}
|
|
385
|
+
onClick={onClick}
|
|
386
|
+
{...props}
|
|
387
|
+
>
|
|
388
|
+
{dropIndicator && (
|
|
389
|
+
<div
|
|
390
|
+
className={cn(
|
|
391
|
+
"absolute left-2 right-2 h-0.5 rounded-full bg-blue-500 z-20 pointer-events-none",
|
|
392
|
+
dropIndicator === "after"
|
|
393
|
+
? "bottom-0 translate-y-1/2"
|
|
394
|
+
: "top-0 -translate-y-1/2",
|
|
395
|
+
)}
|
|
396
|
+
/>
|
|
397
|
+
)}
|
|
398
|
+
<SlideThumbnailCard
|
|
399
|
+
cell={cell}
|
|
400
|
+
isActiveSlide={isActiveSlide}
|
|
401
|
+
isActiveDragSource={isActiveDragSource}
|
|
402
|
+
isVisible={isVisible}
|
|
403
|
+
/>
|
|
404
|
+
</button>
|
|
405
|
+
);
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const SlideThumbnailCard = ({
|
|
409
|
+
cell,
|
|
410
|
+
className,
|
|
411
|
+
style,
|
|
412
|
+
isActiveSlide = false,
|
|
413
|
+
isActiveDragSource = false,
|
|
414
|
+
isOverlay = false,
|
|
415
|
+
isVisible = false,
|
|
416
|
+
ref,
|
|
417
|
+
...props
|
|
418
|
+
}: SlideThumbnailCardProps) => {
|
|
419
|
+
const outerStyle: React.CSSProperties = {
|
|
420
|
+
width: THUMBNAIL_WIDTH,
|
|
421
|
+
height: THUMBNAIL_HEIGHT,
|
|
422
|
+
contain: "strict",
|
|
423
|
+
...(isOverlay
|
|
424
|
+
? null
|
|
425
|
+
: {
|
|
426
|
+
contentVisibility: "auto",
|
|
427
|
+
containIntrinsicSize: `${THUMBNAIL_WIDTH}px ${THUMBNAIL_HEIGHT}px`,
|
|
428
|
+
}),
|
|
429
|
+
...style,
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
const showContent = isVisible || isOverlay;
|
|
433
|
+
|
|
434
|
+
return (
|
|
435
|
+
<div
|
|
436
|
+
ref={ref}
|
|
437
|
+
className={cn(
|
|
438
|
+
"border-2 shrink-0 rounded-md relative select-none bg-background cursor-pointer active:cursor-grabbing",
|
|
439
|
+
isActiveSlide || isActiveDragSource || isOverlay
|
|
440
|
+
? "border-blue-500"
|
|
441
|
+
: "border-border",
|
|
442
|
+
isActiveDragSource && !isOverlay && "opacity-35",
|
|
443
|
+
isOverlay && "opacity-95 shadow-lg",
|
|
444
|
+
className,
|
|
445
|
+
)}
|
|
446
|
+
style={outerStyle}
|
|
447
|
+
{...props}
|
|
448
|
+
>
|
|
449
|
+
{showContent && (
|
|
450
|
+
<div
|
|
451
|
+
className="flex p-6 box-border pointer-events-none mo-slide-content overflow-hidden"
|
|
452
|
+
style={{
|
|
453
|
+
transform: `scale(${THUMBNAIL_SCALE})`,
|
|
454
|
+
transformOrigin: "top left",
|
|
455
|
+
width: THUMBNAIL_WIDTH / THUMBNAIL_SCALE,
|
|
456
|
+
height: THUMBNAIL_HEIGHT / THUMBNAIL_SCALE,
|
|
457
|
+
}}
|
|
458
|
+
>
|
|
459
|
+
<Slide cellId={cell.id} status={cell.status} output={cell.output} />
|
|
460
|
+
</div>
|
|
461
|
+
)}
|
|
462
|
+
</div>
|
|
463
|
+
);
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
function projectDropTarget(
|
|
467
|
+
event: DragMoveEvent | DragOverEvent,
|
|
468
|
+
): ProjectedDropTarget | null {
|
|
469
|
+
const { active, over } = event;
|
|
470
|
+
if (!over) {
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const activeId = asCellId(active.id);
|
|
475
|
+
const overId = asCellId(over.id);
|
|
476
|
+
if (!activeId || !overId || activeId === overId) {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const activeRect =
|
|
481
|
+
active.rect.current.translated ?? active.rect.current.initial;
|
|
482
|
+
if (!activeRect) {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const pointerY = activeRect.top + activeRect.height / 2;
|
|
487
|
+
const overCenter = over.rect.top + over.rect.height / 2;
|
|
488
|
+
|
|
489
|
+
return {
|
|
490
|
+
overId,
|
|
491
|
+
position: pointerY < overCenter ? "before" : "after",
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function resolveDropTarget({
|
|
496
|
+
cellIds,
|
|
497
|
+
activeId,
|
|
498
|
+
target,
|
|
499
|
+
}: {
|
|
500
|
+
cellIds: CellIdsState;
|
|
501
|
+
activeId: CellId;
|
|
502
|
+
target: ProjectedDropTarget;
|
|
503
|
+
}): ResolvedDropTarget | null {
|
|
504
|
+
if (activeId === target.overId) {
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
if (cellIds.colLength !== 1) {
|
|
508
|
+
Logger.warn("Multi-column mode is not supported");
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const column = cellIds.findWithId(target.overId);
|
|
513
|
+
const overIndex = column.indexOfOrThrow(target.overId);
|
|
514
|
+
|
|
515
|
+
return {
|
|
516
|
+
cellId: activeId,
|
|
517
|
+
columnId: column.id,
|
|
518
|
+
index: target.position === "after" ? overIndex + 1 : overIndex,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Narrows a dnd-kit UniqueIdentifier (string | number) back to CellId.
|
|
524
|
+
* Safe because we only pass CellId values as sortable item IDs.
|
|
525
|
+
*/
|
|
526
|
+
function asCellId(id: UniqueIdentifier): CellId | null {
|
|
527
|
+
return typeof id === "string" ? (id as CellId) : null;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
export const exportedForTesting = {
|
|
531
|
+
useVisibleCellIds,
|
|
532
|
+
projectDropTarget,
|
|
533
|
+
resolveDropTarget,
|
|
534
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { outputIsLoading } from "@/core/cells/cell";
|
|
4
|
+
import { OutputArea } from "../editor/Output";
|
|
5
|
+
import { memo } from "react";
|
|
6
|
+
import type { CellId } from "@/core/cells/ids";
|
|
7
|
+
import type { CellRuntimeState } from "@/core/cells/types";
|
|
8
|
+
|
|
9
|
+
interface SlideContentProps extends Pick<
|
|
10
|
+
CellRuntimeState,
|
|
11
|
+
"output" | "status"
|
|
12
|
+
> {
|
|
13
|
+
cellId: CellId;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const Slide = memo(({ output, cellId, status }: SlideContentProps) => {
|
|
17
|
+
const loading = outputIsLoading(status);
|
|
18
|
+
return (
|
|
19
|
+
<OutputArea
|
|
20
|
+
className="contents"
|
|
21
|
+
allowExpand={false}
|
|
22
|
+
output={output}
|
|
23
|
+
cellId={cellId}
|
|
24
|
+
stale={loading}
|
|
25
|
+
loading={loading}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
Slide.displayName = "Slide";
|
|
@@ -22,6 +22,8 @@ interface SlidesComponentProps {
|
|
|
22
22
|
index?: string | null;
|
|
23
23
|
height?: string | number | null;
|
|
24
24
|
wrapAround?: boolean;
|
|
25
|
+
activeIndex?: number | null;
|
|
26
|
+
onActiveIndexChange?: (index: number) => void;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
const SlidesComponent = ({
|
|
@@ -30,11 +32,23 @@ const SlidesComponent = ({
|
|
|
30
32
|
height,
|
|
31
33
|
forceKeyboardNavigation = false,
|
|
32
34
|
wrapAround = false,
|
|
35
|
+
activeIndex,
|
|
36
|
+
onActiveIndexChange,
|
|
33
37
|
}: PropsWithChildren<SlidesComponentProps>): JSX.Element => {
|
|
34
38
|
const el = React.useRef<SwiperRef>(null);
|
|
35
39
|
const [isFullscreen, setIsFullscreen] = React.useState(false);
|
|
36
40
|
const { hasFullscreen } = useIframeCapabilities();
|
|
37
41
|
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (activeIndex != null && el.current?.swiper.realIndex !== activeIndex) {
|
|
44
|
+
if (wrapAround) {
|
|
45
|
+
el.current?.swiper.slideToLoop(activeIndex);
|
|
46
|
+
} else {
|
|
47
|
+
el.current?.swiper.slideTo(activeIndex);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}, [activeIndex, wrapAround]);
|
|
51
|
+
|
|
38
52
|
useEventListener(document, "fullscreenchange", () => {
|
|
39
53
|
if (document.fullscreenElement) {
|
|
40
54
|
el.current?.swiper.keyboard.enable();
|
|
@@ -63,7 +77,7 @@ const SlidesComponent = ({
|
|
|
63
77
|
)}
|
|
64
78
|
spaceBetween={50}
|
|
65
79
|
style={{
|
|
66
|
-
height: isFullscreen ? "100%" : height || "
|
|
80
|
+
height: isFullscreen ? "100%" : height || "650px",
|
|
67
81
|
}}
|
|
68
82
|
slidesPerView={1}
|
|
69
83
|
modules={modules}
|
|
@@ -85,6 +99,7 @@ const SlidesComponent = ({
|
|
|
85
99
|
// that overlay content more legible
|
|
86
100
|
speed={1}
|
|
87
101
|
loop={wrapAround}
|
|
102
|
+
onSlideChange={(swiper) => onActiveIndexChange?.(swiper.realIndex)}
|
|
88
103
|
>
|
|
89
104
|
{React.Children.map(children, (child, index) => {
|
|
90
105
|
if (child == null) {
|