@marimo-team/islands 0.23.3-dev9 → 0.23.3
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/{chat-ui-BLFhPclV.js → chat-ui-DEd_Ndal.js} +82 -82
- package/dist/{html-to-image-XYwXqg2E.js → html-to-image-DBosi5GK.js} +2240 -2214
- package/dist/main.js +2627 -2746
- package/dist/{process-output-BDVjDpbu.js → process-output-k-4WHpxz.js} +1 -1
- package/dist/{reveal-component-CrnLosc4.js → reveal-component-CFuofbBD.js} +827 -561
- package/dist/{slide-Dl7Rf496.js → slide-form-DgMI37ES.js} +1729 -894
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/editor/file-tree/renderers.tsx +1 -1
- package/src/components/editor/output/JsonOutput.tsx +187 -4
- package/src/components/editor/output/__tests__/JsonOutput-mimetype.test.tsx +80 -0
- package/src/components/editor/output/__tests__/json-output.test.ts +185 -2
- 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/islands/__tests__/bridge.test.ts +116 -5
- package/src/core/islands/bridge.ts +5 -1
- package/src/core/layout/layout.ts +6 -2
- package/src/core/static/__tests__/export-context.test.ts +122 -0
- package/src/core/static/__tests__/static-state.test.ts +80 -0
- package/src/core/static/export-context.ts +84 -0
- package/src/core/static/static-state.ts +44 -6
- package/src/plugins/core/RenderHTML.tsx +23 -2
- package/src/plugins/core/__test__/RenderHTML.test.ts +86 -1
- package/src/plugins/core/__test__/trusted-url.test.ts +130 -18
- package/src/plugins/core/sanitize.ts +11 -5
- package/src/plugins/core/trusted-url.ts +32 -10
- package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +29 -1
- package/src/plugins/impl/mpl-interactive/__tests__/MplInteractivePlugin.test.tsx +34 -0
- package/src/plugins/impl/panel/__tests__/PanelPlugin.test.ts +35 -2
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import type { SlideType } from "../editor/renderers/slides-layout/types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A contiguous run of cells that render together on a single subslide.
|
|
7
|
+
*
|
|
8
|
+
* - `isFragment=false`: cells are emitted inline in the <Slide>.
|
|
9
|
+
* - `isFragment=true`: cells are wrapped in a single <Fragment> so they reveal
|
|
10
|
+
* as one step.
|
|
11
|
+
*/
|
|
12
|
+
export interface ComposedBlock<T> {
|
|
13
|
+
isFragment: boolean;
|
|
14
|
+
cells: T[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* One <Slide>. Vertical stacking is expressed at the Stack level.
|
|
19
|
+
*/
|
|
20
|
+
export interface ComposedSubslide<T> {
|
|
21
|
+
blocks: ComposedBlock<T>[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* One horizontal position in the deck.
|
|
26
|
+
*
|
|
27
|
+
* - `subslides.length === 1` -> render as a single <Slide>.
|
|
28
|
+
* - `subslides.length > 1` -> render as <Stack><Slide/>...</Stack>.
|
|
29
|
+
*/
|
|
30
|
+
export interface ComposedStack<T> {
|
|
31
|
+
subslides: ComposedSubslide<T>[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface Composition<T> {
|
|
35
|
+
stacks: ComposedStack<T>[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Groups a flat list of cells into a tree of stacks / subslides / blocks based
|
|
40
|
+
* on each cell's {@link SlideType}.
|
|
41
|
+
*
|
|
42
|
+
* Inspired by the RISE JupyterLab extension's `markupSlides`, but adapted for a
|
|
43
|
+
* declarative React renderer: instead of mutating DOM during the walk, we
|
|
44
|
+
* produce a tree the caller can render however they like.
|
|
45
|
+
*
|
|
46
|
+
* Callers are responsible for normalizing "no type set" to a concrete
|
|
47
|
+
* {@link SlideType} before getting here — the convention in this codebase is
|
|
48
|
+
* that a cell with no configured type is a `"slide"`.
|
|
49
|
+
*
|
|
50
|
+
* Rules (per cell):
|
|
51
|
+
* - `"slide"` -> open a new stack + subslide, cell goes in a plain block.
|
|
52
|
+
* - `"sub-slide"` -> open a new subslide inside the current stack.
|
|
53
|
+
* - `"fragment"` -> open a new fragment block inside the current subslide.
|
|
54
|
+
* - `"skip"` -> dropped entirely. If a caller wants to preserve these,
|
|
55
|
+
* they can remap the type in `getType`.
|
|
56
|
+
*
|
|
57
|
+
* If the very first cell is a `fragment` or `sub-slide`, a containing stack /
|
|
58
|
+
* subslide is created implicitly.
|
|
59
|
+
*/
|
|
60
|
+
export function composeSlides<T>({
|
|
61
|
+
cells,
|
|
62
|
+
getType,
|
|
63
|
+
}: {
|
|
64
|
+
cells: readonly T[];
|
|
65
|
+
getType: (cell: T) => SlideType;
|
|
66
|
+
}): Composition<T> {
|
|
67
|
+
const stacks: ComposedStack<T>[] = [];
|
|
68
|
+
let stack: ComposedStack<T> | null = null;
|
|
69
|
+
let subslide: ComposedSubslide<T> | null = null;
|
|
70
|
+
|
|
71
|
+
const openStack = (): ComposedStack<T> => {
|
|
72
|
+
const next: ComposedStack<T> = { subslides: [] };
|
|
73
|
+
stacks.push(next);
|
|
74
|
+
stack = next;
|
|
75
|
+
subslide = null;
|
|
76
|
+
return next;
|
|
77
|
+
};
|
|
78
|
+
const openSubslide = (): ComposedSubslide<T> => {
|
|
79
|
+
const parent = stack ?? openStack();
|
|
80
|
+
const next: ComposedSubslide<T> = { blocks: [] };
|
|
81
|
+
parent.subslides.push(next);
|
|
82
|
+
subslide = next;
|
|
83
|
+
return next;
|
|
84
|
+
};
|
|
85
|
+
const openBlock = (isFragment: boolean): ComposedBlock<T> => {
|
|
86
|
+
const parent = subslide ?? openSubslide();
|
|
87
|
+
const next: ComposedBlock<T> = { isFragment, cells: [] };
|
|
88
|
+
parent.blocks.push(next);
|
|
89
|
+
return next;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
for (const cell of cells) {
|
|
93
|
+
const type = getType(cell);
|
|
94
|
+
|
|
95
|
+
switch (type) {
|
|
96
|
+
case "skip":
|
|
97
|
+
break;
|
|
98
|
+
case "slide":
|
|
99
|
+
openStack();
|
|
100
|
+
openSubslide();
|
|
101
|
+
openBlock(false).cells.push(cell);
|
|
102
|
+
break;
|
|
103
|
+
case "sub-slide":
|
|
104
|
+
openSubslide();
|
|
105
|
+
openBlock(false).cells.push(cell);
|
|
106
|
+
break;
|
|
107
|
+
case "fragment":
|
|
108
|
+
openBlock(true).cells.push(cell);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { stacks };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* A location within the composed tree. Mirrors reveal.js's `{h, v, f}` indices
|
|
118
|
+
* so callers can feed it directly to `Reveal.slide(h, v, f?)`.
|
|
119
|
+
*
|
|
120
|
+
* - `h`: stack index
|
|
121
|
+
* - `v`: subslide index within that stack
|
|
122
|
+
* - `f`: fragment block index within the subslide, or `-1` for non-fragment
|
|
123
|
+
* cells (i.e. content that's visible before any fragment is revealed).
|
|
124
|
+
*/
|
|
125
|
+
export interface SlideTarget {
|
|
126
|
+
h: number;
|
|
127
|
+
v: number;
|
|
128
|
+
f: number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface SlideIndices<Id> {
|
|
132
|
+
/** Where each cell lives in the deck. */
|
|
133
|
+
cellToTarget: Map<Id, SlideTarget>;
|
|
134
|
+
/**
|
|
135
|
+
* `"h,v,f"` -> flat index into the original cell list. The value is the
|
|
136
|
+
* "active" cell for that position — i.e. the last cell currently visible
|
|
137
|
+
* on screen, so the active cell advances as the user steps through
|
|
138
|
+
* fragments.
|
|
139
|
+
*
|
|
140
|
+
* `f === -1` represents "nothing revealed yet" and is always populated when
|
|
141
|
+
* a subslide exists, so it doubles as a fallback for subslides that have
|
|
142
|
+
* no fragments at all (reveal reports `f === 0` in that case).
|
|
143
|
+
*/
|
|
144
|
+
targetToCellIndex: Map<string, number>;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Build {@link SlideIndices} for a composition so callers can translate
|
|
149
|
+
* between a flat cell list and reveal.js's `{h, v, f}` indices.
|
|
150
|
+
*/
|
|
151
|
+
export function buildSlideIndices<T, Id>({
|
|
152
|
+
composition,
|
|
153
|
+
cells,
|
|
154
|
+
getId,
|
|
155
|
+
}: {
|
|
156
|
+
composition: Composition<T>;
|
|
157
|
+
cells: readonly T[];
|
|
158
|
+
getId: (cell: T) => Id;
|
|
159
|
+
}): SlideIndices<Id> {
|
|
160
|
+
const cellToTarget = new Map<Id, SlideTarget>();
|
|
161
|
+
const targetToCellIndex = new Map<string, number>();
|
|
162
|
+
const cellIndexById = new Map<Id, number>();
|
|
163
|
+
for (const [i, cell] of cells.entries()) {
|
|
164
|
+
cellIndexById.set(getId(cell), i);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
composition.stacks.forEach((stack, h) => {
|
|
168
|
+
stack.subslides.forEach((sub, v) => {
|
|
169
|
+
// f = -1: "nothing revealed yet" state. Active cell = last cell that
|
|
170
|
+
// appears before the first fragment block. If the subslide starts with
|
|
171
|
+
// a fragment block, fall back to its first cell so we still have an
|
|
172
|
+
// anchor when nothing is visible yet.
|
|
173
|
+
let preFragmentActiveId: Id | undefined;
|
|
174
|
+
for (const block of sub.blocks) {
|
|
175
|
+
if (block.isFragment) {
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
const last = block.cells.at(-1);
|
|
179
|
+
if (last) {
|
|
180
|
+
preFragmentActiveId = getId(last);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (preFragmentActiveId === undefined) {
|
|
184
|
+
const fallback = sub.blocks[0]?.cells[0];
|
|
185
|
+
if (fallback) {
|
|
186
|
+
preFragmentActiveId = getId(fallback);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (preFragmentActiveId !== undefined) {
|
|
190
|
+
const idx = cellIndexById.get(preFragmentActiveId);
|
|
191
|
+
if (idx != null) {
|
|
192
|
+
targetToCellIndex.set(`${h},${v},-1`, idx);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let fragmentCounter = -1;
|
|
197
|
+
for (const block of sub.blocks) {
|
|
198
|
+
if (block.isFragment) {
|
|
199
|
+
fragmentCounter++;
|
|
200
|
+
for (const cell of block.cells) {
|
|
201
|
+
cellToTarget.set(getId(cell), { h, v, f: fragmentCounter });
|
|
202
|
+
}
|
|
203
|
+
const last = block.cells.at(-1);
|
|
204
|
+
if (last) {
|
|
205
|
+
const idx = cellIndexById.get(getId(last));
|
|
206
|
+
if (idx != null) {
|
|
207
|
+
targetToCellIndex.set(`${h},${v},${fragmentCounter}`, idx);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
for (const cell of block.cells) {
|
|
212
|
+
cellToTarget.set(getId(cell), { h, v, f: -1 });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
return { cellToTarget, targetToCellIndex };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Resolve the flat cell index for the current reveal indices. For slides
|
|
223
|
+
* without fragments reveal may report `f = 0`, so we fall back to the `-1`
|
|
224
|
+
* entry (which is always populated when a subslide exists).
|
|
225
|
+
*/
|
|
226
|
+
export function resolveActiveCellIndex(
|
|
227
|
+
targetToCellIndex: ReadonlyMap<string, number>,
|
|
228
|
+
indices: { h: number; v: number; f: number },
|
|
229
|
+
): number | undefined {
|
|
230
|
+
const { h, v, f } = indices;
|
|
231
|
+
return (
|
|
232
|
+
targetToCellIndex.get(`${h},${v},${f}`) ??
|
|
233
|
+
targetToCellIndex.get(`${h},${v},-1`)
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function findDeckTarget<T, Id>({
|
|
238
|
+
start,
|
|
239
|
+
stop,
|
|
240
|
+
step,
|
|
241
|
+
cells,
|
|
242
|
+
cellToTarget,
|
|
243
|
+
getId,
|
|
244
|
+
}: {
|
|
245
|
+
start: number;
|
|
246
|
+
stop: number;
|
|
247
|
+
step: 1 | -1;
|
|
248
|
+
cells: readonly T[];
|
|
249
|
+
cellToTarget: ReadonlyMap<Id, SlideTarget>;
|
|
250
|
+
getId: (cell: T) => Id;
|
|
251
|
+
}): SlideTarget | undefined {
|
|
252
|
+
for (let i = start; i !== stop; i += step) {
|
|
253
|
+
const target = cellToTarget.get(getId(cells[i]));
|
|
254
|
+
if (target) {
|
|
255
|
+
return target;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Resolve the deck location that should back the currently selected minimap
|
|
263
|
+
* entry.
|
|
264
|
+
*
|
|
265
|
+
* Cells marked `"skip"` are not part of the composed deck; for those we
|
|
266
|
+
* "park" on the closest real slide in notebook order (preferring the
|
|
267
|
+
* predecessor). Handling the UI consequences of parking — ignoring reveal.js
|
|
268
|
+
* echo events and intercepting keyboard nav — is the caller's job.
|
|
269
|
+
*/
|
|
270
|
+
export function resolveDeckNavigationTarget<T, Id>({
|
|
271
|
+
activeIndex,
|
|
272
|
+
cells,
|
|
273
|
+
cellToTarget,
|
|
274
|
+
getId,
|
|
275
|
+
}: {
|
|
276
|
+
activeIndex: number | undefined;
|
|
277
|
+
cells: readonly T[];
|
|
278
|
+
cellToTarget: ReadonlyMap<Id, SlideTarget>;
|
|
279
|
+
getId: (cell: T) => Id;
|
|
280
|
+
}): SlideTarget | undefined {
|
|
281
|
+
if (activeIndex == null) {
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const activeCell = cells[activeIndex];
|
|
286
|
+
if (!activeCell) {
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const directTarget = cellToTarget.get(getId(activeCell));
|
|
291
|
+
if (directTarget) {
|
|
292
|
+
return directTarget;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return (
|
|
296
|
+
findDeckTarget({
|
|
297
|
+
start: activeIndex - 1,
|
|
298
|
+
stop: -1,
|
|
299
|
+
step: -1,
|
|
300
|
+
cells,
|
|
301
|
+
cellToTarget,
|
|
302
|
+
getId,
|
|
303
|
+
}) ??
|
|
304
|
+
findDeckTarget({
|
|
305
|
+
start: activeIndex + 1,
|
|
306
|
+
stop: cells.length,
|
|
307
|
+
step: 1,
|
|
308
|
+
cells,
|
|
309
|
+
cellToTarget,
|
|
310
|
+
getId,
|
|
311
|
+
})
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Decide where the deck should navigate to given the user's target cell, or
|
|
317
|
+
* `null` if the deck is already there (so the caller can skip the imperative
|
|
318
|
+
* `deck.slide()` call).
|
|
319
|
+
*
|
|
320
|
+
* Each minimap entry is a distinct navigation target, so the fragment index
|
|
321
|
+
* is always pinned:
|
|
322
|
+
* - fragment cells (`target.f >= 0`) advance to that fragment
|
|
323
|
+
* - non-fragment cells reset to `f = -1` so any revealed fragments on the
|
|
324
|
+
* destination slide collapse — otherwise jumping to the parent slide
|
|
325
|
+
* from elsewhere in the deck leaves fragments looking "completed"
|
|
326
|
+
* instead of clean.
|
|
327
|
+
*/
|
|
328
|
+
export function computeDeckNavigation(
|
|
329
|
+
current: { h: number; v: number; f: number },
|
|
330
|
+
target: SlideTarget,
|
|
331
|
+
): SlideTarget | null {
|
|
332
|
+
const next = { h: target.h, v: target.v, f: target.f >= 0 ? target.f : -1 };
|
|
333
|
+
if (current.h === next.h && current.v === next.v && current.f === next.f) {
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
return next;
|
|
337
|
+
}
|
|
@@ -5,7 +5,10 @@ import type { CellId } from "@/core/cells/ids";
|
|
|
5
5
|
import type { CellColumnId } from "@/utils/id-tree";
|
|
6
6
|
import { useEffect, useRef, useState } from "react";
|
|
7
7
|
import type { ICellRendererProps } from "../editor/renderers/types";
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
SlideType,
|
|
10
|
+
SlidesLayout,
|
|
11
|
+
} from "../editor/renderers/slides-layout/types";
|
|
9
12
|
import {
|
|
10
13
|
DndContext,
|
|
11
14
|
DragOverlay,
|
|
@@ -29,8 +32,10 @@ import {
|
|
|
29
32
|
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
|
|
30
33
|
import { cn } from "@/utils/cn";
|
|
31
34
|
import { Slide } from "./slide";
|
|
32
|
-
import { InfoIcon } from "lucide-react";
|
|
35
|
+
import { InfoIcon, type LucideIcon } from "lucide-react";
|
|
36
|
+
import { Tooltip } from "@/components/ui/tooltip";
|
|
33
37
|
import { Logger } from "@/utils/Logger";
|
|
38
|
+
import { SLIDE_TYPE_OPTIONS_BY_VALUE } from "./slide-form";
|
|
34
39
|
|
|
35
40
|
type Props = ICellRendererProps<SlidesLayout>;
|
|
36
41
|
type SlideCell = Props["cells"][number];
|
|
@@ -48,34 +53,65 @@ interface ResolvedDropTarget {
|
|
|
48
53
|
index: number;
|
|
49
54
|
}
|
|
50
55
|
|
|
56
|
+
interface ThumbnailDimensions {
|
|
57
|
+
width: number;
|
|
58
|
+
height: number;
|
|
59
|
+
scale: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
51
62
|
interface SlideThumbnailCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
52
63
|
cell: SlideCell;
|
|
64
|
+
dimensions: ThumbnailDimensions;
|
|
53
65
|
isActiveSlide?: boolean;
|
|
54
66
|
isActiveDragSource?: boolean;
|
|
55
67
|
isOverlay?: boolean;
|
|
56
68
|
isVisible?: boolean;
|
|
69
|
+
slideType?: SlideType;
|
|
57
70
|
ref?: React.Ref<HTMLDivElement>;
|
|
58
71
|
}
|
|
59
72
|
|
|
60
73
|
interface SlideThumbnailRowProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
61
74
|
cell: SlideCell;
|
|
75
|
+
dimensions: ThumbnailDimensions;
|
|
62
76
|
isActiveSlide?: boolean;
|
|
63
77
|
dropIndicator?: DropPosition | null;
|
|
64
78
|
isActiveDragSource?: boolean;
|
|
65
79
|
isVisible?: boolean;
|
|
80
|
+
slideType?: SlideType;
|
|
66
81
|
ref?: React.Ref<HTMLButtonElement>;
|
|
67
82
|
}
|
|
68
83
|
|
|
69
84
|
interface SlidesMinimapProps {
|
|
70
85
|
cells: SlideCell[];
|
|
86
|
+
thumbnailWidth: number;
|
|
71
87
|
canReorder: boolean;
|
|
72
88
|
activeCellId: CellId | null;
|
|
89
|
+
// Set of cell ids that are marked `skip` in the slides layout.
|
|
90
|
+
skippedIds?: ReadonlySet<CellId>;
|
|
91
|
+
slideTypes?: ReadonlyMap<CellId, SlideType>;
|
|
73
92
|
onSlideClick: (index: number) => void;
|
|
74
93
|
}
|
|
75
94
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
95
|
+
function getSlideTypeVisual(
|
|
96
|
+
slideType: SlideType | undefined,
|
|
97
|
+
): { label: string; description: string; Icon: LucideIcon } | null {
|
|
98
|
+
if (!slideType || slideType === "slide") {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const { label, description, Icon } = SLIDE_TYPE_OPTIONS_BY_VALUE[slideType];
|
|
102
|
+
return { label, description, Icon };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const SLIDE_ASPECT_RATIO = 16 / 9;
|
|
106
|
+
const SLIDE_BASE_WIDTH = 960;
|
|
107
|
+
|
|
108
|
+
function computeThumbnailDimensions(width: number): ThumbnailDimensions {
|
|
109
|
+
return {
|
|
110
|
+
width,
|
|
111
|
+
height: width / SLIDE_ASPECT_RATIO,
|
|
112
|
+
scale: width / SLIDE_BASE_WIDTH,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
79
115
|
const MINIMAP_AUTO_SCROLL = {
|
|
80
116
|
threshold: { x: 0, y: 0.1 },
|
|
81
117
|
};
|
|
@@ -156,8 +192,11 @@ function useVisibleCellIds(
|
|
|
156
192
|
|
|
157
193
|
export const SlidesMinimap = ({
|
|
158
194
|
cells,
|
|
195
|
+
thumbnailWidth,
|
|
159
196
|
canReorder,
|
|
160
197
|
activeCellId,
|
|
198
|
+
skippedIds,
|
|
199
|
+
slideTypes,
|
|
161
200
|
onSlideClick,
|
|
162
201
|
}: SlidesMinimapProps) => {
|
|
163
202
|
const cellIds = useCellIds();
|
|
@@ -168,6 +207,7 @@ export const SlidesMinimap = ({
|
|
|
168
207
|
const [dropTarget, setDropTarget] = useState<ProjectedDropTarget | null>(
|
|
169
208
|
null,
|
|
170
209
|
);
|
|
210
|
+
const dimensions = computeThumbnailDimensions(thumbnailWidth);
|
|
171
211
|
|
|
172
212
|
useEffect(() => {
|
|
173
213
|
if (!activeCellId || !containerRef.current) {
|
|
@@ -245,8 +285,14 @@ export const SlidesMinimap = ({
|
|
|
245
285
|
<SlideThumbnailRow
|
|
246
286
|
key={cell.id}
|
|
247
287
|
cell={cell}
|
|
288
|
+
dimensions={dimensions}
|
|
248
289
|
isActiveSlide={cell.id === activeCellId}
|
|
249
290
|
isVisible={visibleIds.has(cell.id)}
|
|
291
|
+
slideType={resolveSlideType({
|
|
292
|
+
cellId: cell.id,
|
|
293
|
+
slideTypes,
|
|
294
|
+
skippedIds,
|
|
295
|
+
})}
|
|
250
296
|
onClick={() => onSlideClick(index)}
|
|
251
297
|
/>
|
|
252
298
|
))}
|
|
@@ -275,9 +321,15 @@ export const SlidesMinimap = ({
|
|
|
275
321
|
<SortableSlideThumbnail
|
|
276
322
|
key={cell.id}
|
|
277
323
|
cell={cell}
|
|
324
|
+
dimensions={dimensions}
|
|
278
325
|
isActive={activeId === cell.id}
|
|
279
326
|
isActiveSlide={cell.id === activeCellId}
|
|
280
327
|
isVisible={visibleIds.has(cell.id)}
|
|
328
|
+
slideType={resolveSlideType({
|
|
329
|
+
cellId: cell.id,
|
|
330
|
+
slideTypes,
|
|
331
|
+
skippedIds,
|
|
332
|
+
})}
|
|
281
333
|
dropIndicator={
|
|
282
334
|
dropTarget?.overId === cell.id && activeId !== cell.id
|
|
283
335
|
? dropTarget.position
|
|
@@ -292,6 +344,7 @@ export const SlidesMinimap = ({
|
|
|
292
344
|
{activeCell && (
|
|
293
345
|
<SlideThumbnailCard
|
|
294
346
|
cell={activeCell}
|
|
347
|
+
dimensions={dimensions}
|
|
295
348
|
isOverlay={true}
|
|
296
349
|
isActiveDragSource={true}
|
|
297
350
|
/>
|
|
@@ -320,19 +373,23 @@ const SlideThumbnailsContainer = ({
|
|
|
320
373
|
|
|
321
374
|
interface SortableSlideThumbnailProps {
|
|
322
375
|
cell: SlideCell;
|
|
376
|
+
dimensions: ThumbnailDimensions;
|
|
323
377
|
dropIndicator?: DropPosition | null;
|
|
324
378
|
isActive: boolean;
|
|
325
379
|
isActiveSlide?: boolean;
|
|
326
380
|
isVisible?: boolean;
|
|
381
|
+
slideType?: SlideType;
|
|
327
382
|
onClick?: () => void;
|
|
328
383
|
}
|
|
329
384
|
|
|
330
385
|
const SortableSlideThumbnail = ({
|
|
331
386
|
cell,
|
|
387
|
+
dimensions,
|
|
332
388
|
dropIndicator,
|
|
333
389
|
isActive,
|
|
334
390
|
isActiveSlide,
|
|
335
391
|
isVisible,
|
|
392
|
+
slideType,
|
|
336
393
|
onClick,
|
|
337
394
|
}: SortableSlideThumbnailProps) => {
|
|
338
395
|
const { attributes, listeners, setNodeRef } = useSortable({
|
|
@@ -343,10 +400,12 @@ const SortableSlideThumbnail = ({
|
|
|
343
400
|
<SlideThumbnailRow
|
|
344
401
|
ref={setNodeRef}
|
|
345
402
|
cell={cell}
|
|
403
|
+
dimensions={dimensions}
|
|
346
404
|
dropIndicator={dropIndicator}
|
|
347
405
|
isActiveDragSource={isActive}
|
|
348
406
|
isActiveSlide={isActiveSlide}
|
|
349
407
|
isVisible={isVisible}
|
|
408
|
+
slideType={slideType}
|
|
350
409
|
onClick={onClick}
|
|
351
410
|
{...attributes}
|
|
352
411
|
{...listeners}
|
|
@@ -356,12 +415,14 @@ const SortableSlideThumbnail = ({
|
|
|
356
415
|
|
|
357
416
|
const SlideThumbnailRow = ({
|
|
358
417
|
cell,
|
|
418
|
+
dimensions,
|
|
359
419
|
className,
|
|
360
420
|
style,
|
|
361
421
|
dropIndicator,
|
|
362
422
|
isActiveSlide = false,
|
|
363
423
|
isActiveDragSource = false,
|
|
364
424
|
isVisible,
|
|
425
|
+
slideType,
|
|
365
426
|
onClick,
|
|
366
427
|
ref,
|
|
367
428
|
...props
|
|
@@ -397,9 +458,11 @@ const SlideThumbnailRow = ({
|
|
|
397
458
|
)}
|
|
398
459
|
<SlideThumbnailCard
|
|
399
460
|
cell={cell}
|
|
461
|
+
dimensions={dimensions}
|
|
400
462
|
isActiveSlide={isActiveSlide}
|
|
401
463
|
isActiveDragSource={isActiveDragSource}
|
|
402
464
|
isVisible={isVisible}
|
|
465
|
+
slideType={slideType}
|
|
403
466
|
/>
|
|
404
467
|
</button>
|
|
405
468
|
);
|
|
@@ -407,24 +470,30 @@ const SlideThumbnailRow = ({
|
|
|
407
470
|
|
|
408
471
|
const SlideThumbnailCard = ({
|
|
409
472
|
cell,
|
|
473
|
+
dimensions,
|
|
410
474
|
className,
|
|
411
475
|
style,
|
|
412
476
|
isActiveSlide = false,
|
|
413
477
|
isActiveDragSource = false,
|
|
414
478
|
isOverlay = false,
|
|
415
479
|
isVisible = false,
|
|
480
|
+
slideType,
|
|
416
481
|
ref,
|
|
417
482
|
...props
|
|
418
483
|
}: SlideThumbnailCardProps) => {
|
|
484
|
+
const { width, height, scale } = dimensions;
|
|
485
|
+
const visual = getSlideTypeVisual(slideType);
|
|
486
|
+
const isSkipped = slideType === "skip";
|
|
487
|
+
|
|
419
488
|
const outerStyle: React.CSSProperties = {
|
|
420
|
-
width
|
|
421
|
-
height
|
|
489
|
+
width,
|
|
490
|
+
height,
|
|
422
491
|
contain: "strict",
|
|
423
492
|
...(isOverlay
|
|
424
493
|
? null
|
|
425
494
|
: {
|
|
426
495
|
contentVisibility: "auto",
|
|
427
|
-
containIntrinsicSize: `${
|
|
496
|
+
containIntrinsicSize: `${width}px ${height}px`,
|
|
428
497
|
}),
|
|
429
498
|
...style,
|
|
430
499
|
};
|
|
@@ -435,7 +504,7 @@ const SlideThumbnailCard = ({
|
|
|
435
504
|
<div
|
|
436
505
|
ref={ref}
|
|
437
506
|
className={cn(
|
|
438
|
-
"border-2 shrink-0 rounded-md relative select-none bg-background cursor-pointer active:cursor-grabbing",
|
|
507
|
+
"border-2 shrink-0 rounded-md relative select-none bg-background cursor-pointer active:cursor-grabbing overflow-hidden",
|
|
439
508
|
isActiveSlide || isActiveDragSource || isOverlay
|
|
440
509
|
? "border-blue-500"
|
|
441
510
|
: "border-border",
|
|
@@ -450,15 +519,41 @@ const SlideThumbnailCard = ({
|
|
|
450
519
|
<div
|
|
451
520
|
className="flex p-6 box-border pointer-events-none mo-slide-content overflow-hidden"
|
|
452
521
|
style={{
|
|
453
|
-
transform: `scale(${
|
|
522
|
+
transform: `scale(${scale})`,
|
|
454
523
|
transformOrigin: "top left",
|
|
455
|
-
width:
|
|
456
|
-
height:
|
|
524
|
+
width: width / scale,
|
|
525
|
+
height: height / scale,
|
|
457
526
|
}}
|
|
458
527
|
>
|
|
459
528
|
<Slide cellId={cell.id} status={cell.status} output={cell.output} />
|
|
460
529
|
</div>
|
|
461
530
|
)}
|
|
531
|
+
{isSkipped && (
|
|
532
|
+
<div
|
|
533
|
+
className="absolute inset-0 bg-muted/60 pointer-events-none"
|
|
534
|
+
aria-hidden={true}
|
|
535
|
+
/>
|
|
536
|
+
)}
|
|
537
|
+
{visual && (
|
|
538
|
+
<Tooltip
|
|
539
|
+
content={
|
|
540
|
+
<span className="text-xs opacity-80">{visual.description}</span>
|
|
541
|
+
}
|
|
542
|
+
>
|
|
543
|
+
<span
|
|
544
|
+
className={cn(
|
|
545
|
+
"absolute top-1 right-1 flex items-center gap-1 rounded-full border p-0.5 font-medium leading-none shadow-xs cursor-help",
|
|
546
|
+
isSkipped
|
|
547
|
+
? "bg-background/90 border-border text-muted-foreground"
|
|
548
|
+
: "bg-background/95 border-border text-foreground/80",
|
|
549
|
+
)}
|
|
550
|
+
aria-label={visual.label}
|
|
551
|
+
>
|
|
552
|
+
<visual.Icon className="h-3.5 w-3.5" />
|
|
553
|
+
{/* <span>{visual.label}</span> */}
|
|
554
|
+
</span>
|
|
555
|
+
</Tooltip>
|
|
556
|
+
)}
|
|
462
557
|
</div>
|
|
463
558
|
);
|
|
464
559
|
};
|
|
@@ -527,6 +622,32 @@ function asCellId(id: UniqueIdentifier): CellId | null {
|
|
|
527
622
|
return typeof id === "string" ? (id as CellId) : null;
|
|
528
623
|
}
|
|
529
624
|
|
|
625
|
+
/**
|
|
626
|
+
* Resolves the effective slide type for a cell. Falls back to `"skip"` if the
|
|
627
|
+
* caller-supplied `skippedIds` set marks the cell as skipped, which keeps this
|
|
628
|
+
* component working when only the legacy `skippedIds` prop is provided.
|
|
629
|
+
* Returns `undefined` for the default `"slide"` type so callers can skip
|
|
630
|
+
* rendering any badge.
|
|
631
|
+
*/
|
|
632
|
+
function resolveSlideType({
|
|
633
|
+
cellId,
|
|
634
|
+
slideTypes,
|
|
635
|
+
skippedIds,
|
|
636
|
+
}: {
|
|
637
|
+
cellId: CellId;
|
|
638
|
+
slideTypes: ReadonlyMap<CellId, SlideType> | undefined;
|
|
639
|
+
skippedIds: ReadonlySet<CellId> | undefined;
|
|
640
|
+
}): SlideType | undefined {
|
|
641
|
+
const type = slideTypes?.get(cellId);
|
|
642
|
+
if (type && type !== "slide") {
|
|
643
|
+
return type;
|
|
644
|
+
}
|
|
645
|
+
if (skippedIds?.has(cellId)) {
|
|
646
|
+
return "skip";
|
|
647
|
+
}
|
|
648
|
+
return undefined;
|
|
649
|
+
}
|
|
650
|
+
|
|
530
651
|
export const exportedForTesting = {
|
|
531
652
|
useVisibleCellIds,
|
|
532
653
|
projectDropTarget,
|