@marimo-team/islands 0.23.9-dev44 → 0.23.9-dev46

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.23.9-dev44",
3
+ "version": "0.23.9-dev46",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -65,6 +65,11 @@ export interface CellEditorProps
65
65
  hasOutput?: boolean;
66
66
  languageAdapter: LanguageAdapterType | undefined;
67
67
  showLanguageToggles?: boolean;
68
+ /**
69
+ * Override for the inline "Edit with AI" tooltip. Defaults to the user's
70
+ * `ai.inline_tooltip` config. Set to `false` to force-disable it.
71
+ */
72
+ inlineAiTooltip?: boolean;
68
73
  setLanguageAdapter: React.Dispatch<
69
74
  React.SetStateAction<LanguageAdapterType | undefined>
70
75
  >;
@@ -73,6 +78,12 @@ export interface CellEditorProps
73
78
  editorViewParentRef?: React.RefObject<HTMLDivElement | null>;
74
79
  showHiddenCode: (opts?: { focus?: boolean }) => void;
75
80
  outputArea?: "above" | "below";
81
+ /**
82
+ * CSS selector for the element that editor tooltips (completions, hover,
83
+ * signature help) are appended to. Useful for fullscreen/dialog containers;
84
+ * defaults to `#App`.
85
+ */
86
+ tooltipParentSelector?: string;
76
87
  }
77
88
 
78
89
  const CellEditorInternal = ({
@@ -94,7 +105,9 @@ const CellEditorInternal = ({
94
105
  languageAdapter,
95
106
  setLanguageAdapter,
96
107
  showLanguageToggles = true,
108
+ inlineAiTooltip,
97
109
  outputArea,
110
+ tooltipParentSelector,
98
111
  }: CellEditorProps) => {
99
112
  const [aiCompletionCell, setAiCompletionCell] = useAtom(aiCompletionCellAtom);
100
113
  const deleteCell = useDeleteCellCallback();
@@ -224,7 +237,9 @@ const CellEditorInternal = ({
224
237
  hotkeys: new OverridingHotkeyProvider(userConfig.keymap.overrides ?? {}),
225
238
  diagnosticsConfig: userConfig.diagnostics,
226
239
  displayConfig: userConfig.display,
227
- inlineAiTooltip: userConfig.ai?.inline_tooltip ?? false,
240
+ inlineAiTooltip:
241
+ inlineAiTooltip ?? userConfig.ai?.inline_tooltip ?? false,
242
+ tooltipParentSelector,
228
243
  });
229
244
 
230
245
  extensions.push(
@@ -274,6 +289,8 @@ const CellEditorInternal = ({
274
289
  userConfig.display,
275
290
  userConfig.diagnostics,
276
291
  userConfig.ai?.inline_tooltip,
292
+ inlineAiTooltip,
293
+ tooltipParentSelector,
277
294
  aiFeaturesEnabled,
278
295
  theme,
279
296
  showPlaceholder,
@@ -26,8 +26,9 @@ const layoutOf = (entries: Array<[string, SlideConfig]>): SlidesLayout => ({
26
26
  describe("computeSlideCellsInfo", () => {
27
27
  it("returns empty results for empty input", () => {
28
28
  const result = computeSlideCellsInfo([], layoutOf([]));
29
- expect(result.cellsWithOutput).toEqual([]);
29
+ expect(result.slideCells).toEqual([]);
30
30
  expect(result.skippedIds.size).toBe(0);
31
+ expect(result.noOutputIds.size).toBe(0);
31
32
  expect(result.slideTypes.size).toBe(0);
32
33
  expect(result.startCellIndex).toBe(0);
33
34
  });
@@ -62,22 +63,26 @@ describe("computeSlideCellsInfo", () => {
62
63
  expect(result.startCellIndex).toBe(0);
63
64
  });
64
65
 
65
- it("filters out cells with no output", () => {
66
+ it("keeps cells with no output for the minimap", () => {
66
67
  const result = computeSlideCellsInfo(
67
68
  [cell("a"), cell("b", null), cell("c")],
68
69
  layoutOf([]),
69
70
  );
70
- expect(result.cellsWithOutput.map((c) => c.id)).toEqual(["a", "c"]);
71
+ expect(result.slideCells.map((c) => c.id)).toEqual(["a", "b", "c"]);
72
+ expect([...result.noOutputIds]).toEqual(["b"]);
73
+ expect([...result.skippedIds]).toEqual(["b"]);
71
74
  });
72
75
 
73
- it("filters out cells whose output data is empty string", () => {
76
+ it("keeps cells whose output data is empty string for the minimap", () => {
74
77
  // Mirrors the editor contract: an explicit empty-string payload means the
75
- // cell rendered nothing, so it should not occupy a slide.
78
+ // cell rendered nothing, so it should not occupy a reveal slide.
76
79
  const result = computeSlideCellsInfo(
77
80
  [cell("a"), cell("b", { data: "" }), cell("c")],
78
81
  layoutOf([]),
79
82
  );
80
- expect(result.cellsWithOutput.map((c) => c.id)).toEqual(["a", "c"]);
83
+ expect(result.slideCells.map((c) => c.id)).toEqual(["a", "b", "c"]);
84
+ expect([...result.noOutputIds]).toEqual(["b"]);
85
+ expect([...result.skippedIds]).toEqual(["b"]);
81
86
  });
82
87
 
83
88
  it("keeps cells whose output data is a non-empty value (including falsy ones)", () => {
@@ -91,7 +96,8 @@ describe("computeSlideCellsInfo", () => {
91
96
  ],
92
97
  layoutOf([]),
93
98
  );
94
- expect(result.cellsWithOutput.map((c) => c.id)).toEqual(["a", "b", "c"]);
99
+ expect(result.slideCells.map((c) => c.id)).toEqual(["a", "b", "c"]);
100
+ expect(result.noOutputIds.size).toBe(0);
95
101
  });
96
102
 
97
103
  it("populates slideTypes only for cells with an explicit type", () => {
@@ -121,14 +127,12 @@ describe("computeSlideCellsInfo", () => {
121
127
  expect([...result.skippedIds]).toEqual(["b", "c"]);
122
128
  // Skipped cells are still "visible" deck cells — they just aren't rendered
123
129
  // in reveal. The minimap relies on the full list plus skippedIds.
124
- expect(result.cellsWithOutput.map((c) => c.id)).toEqual(["a", "b", "c"]);
130
+ expect(result.slideCells.map((c) => c.id)).toEqual(["a", "b", "c"]);
125
131
  expect(result.slideTypes.get(cellId("b"))).toBe("skip");
126
132
  });
127
133
 
128
- it("ignores layout entries for cells that have no output", () => {
129
- // If a cell was skipped in the layout but no longer produces output (e.g.
130
- // the user deleted its code), it should drop out of both maps — otherwise
131
- // the skip set would reference ghosts.
134
+ it("preserves configured slide types for cells that have no output", () => {
135
+ // The missing output is transient runtime state, not persisted slide config.
132
136
  const result = computeSlideCellsInfo(
133
137
  [cell("a"), cell("b", null)],
134
138
  layoutOf([
@@ -136,16 +140,25 @@ describe("computeSlideCellsInfo", () => {
136
140
  ["b", { type: "skip" }],
137
141
  ]),
138
142
  );
139
- expect(result.cellsWithOutput.map((c) => c.id)).toEqual(["a"]);
140
- expect(result.skippedIds.size).toBe(0);
141
- expect(result.slideTypes.has(cellId("b"))).toBe(false);
143
+ expect(result.slideCells.map((c) => c.id)).toEqual(["a", "b"]);
144
+ expect([...result.noOutputIds]).toEqual(["b"]);
145
+ expect([...result.skippedIds]).toEqual(["b"]);
146
+ expect(result.slideTypes.get(cellId("b"))).toBe("skip");
147
+ });
148
+
149
+ it("skips no-output cells when computing the starting cell", () => {
150
+ const result = computeSlideCellsInfo(
151
+ [cell("a", null), cell("b", { data: "" }), cell("c")],
152
+ layoutOf([]),
153
+ );
154
+ expect(result.startCellIndex).toBe(2);
142
155
  });
143
156
 
144
- it("preserves the input order of cells in cellsWithOutput", () => {
157
+ it("preserves the input order of cells in slideCells", () => {
145
158
  const result = computeSlideCellsInfo(
146
159
  [cell("c"), cell("a"), cell("b")],
147
160
  layoutOf([]),
148
161
  );
149
- expect(result.cellsWithOutput.map((c) => c.id)).toEqual(["c", "a", "b"]);
162
+ expect(result.slideCells.map((c) => c.id)).toEqual(["c", "a", "b"]);
150
163
  });
151
164
  });
@@ -9,32 +9,40 @@ export interface SlideCellLike {
9
9
  }
10
10
 
11
11
  export interface SlideCellsInfo<T extends SlideCellLike> {
12
- cellsWithOutput: T[];
12
+ slideCells: T[];
13
13
  skippedIds: Set<CellId>;
14
+ noOutputIds: Set<CellId>;
14
15
  slideTypes: Map<CellId, SlideType>;
15
- // Index of the first cell in `cellsWithOutput` that is not skipped
16
+ // Index of the first cell in `slideCells` that is not effectively skipped.
16
17
  startCellIndex: number;
17
18
  }
18
19
 
20
+ export function hasRenderableOutput(cell: SlideCellLike): boolean {
21
+ return cell.output != null && cell.output.data !== "";
22
+ }
23
+
19
24
  export function computeSlideCellsInfo<T extends SlideCellLike>(
20
25
  cells: readonly T[],
21
26
  layout: Pick<SlidesLayout, "cells">,
22
27
  ): SlideCellsInfo<T> {
23
- const cellsWithOutput = cells.filter(
24
- (cell) => cell.output != null && cell.output.data !== "",
25
- );
28
+ const slideCells = [...cells];
26
29
  const skippedIds = new Set<CellId>();
30
+ const noOutputIds = new Set<CellId>();
27
31
  const slideTypes = new Map<CellId, SlideType>();
28
32
 
29
33
  let startCell: T | null = null;
30
34
  let startCellIndex = 0;
31
35
 
32
- for (const [index, cell] of cellsWithOutput.entries()) {
36
+ for (const [index, cell] of slideCells.entries()) {
33
37
  const type = layout.cells.get(cell.id)?.type;
38
+ const hasOutput = hasRenderableOutput(cell);
34
39
  if (type) {
35
40
  slideTypes.set(cell.id, type);
36
41
  }
37
- if (type === "skip") {
42
+ if (!hasOutput) {
43
+ noOutputIds.add(cell.id);
44
+ }
45
+ if (type === "skip" || !hasOutput) {
38
46
  skippedIds.add(cell.id);
39
47
  } else if (startCell === null) {
40
48
  startCell = cell;
@@ -42,8 +50,9 @@ export function computeSlideCellsInfo<T extends SlideCellLike>(
42
50
  }
43
51
  }
44
52
  return {
45
- cellsWithOutput,
53
+ slideCells,
46
54
  skippedIds,
55
+ noOutputIds,
47
56
  slideTypes,
48
57
  startCellIndex,
49
58
  };
@@ -30,19 +30,17 @@ export const SlidesLayoutRenderer: React.FC<Props> = ({
30
30
  const isMultiColumn = numColumns > 1;
31
31
  const [activeCellId, setActiveCellId] = useState<CellId | null>(null);
32
32
 
33
- const { cellsWithOutput, skippedIds, slideTypes, startCellIndex } = useMemo(
34
- () => computeSlideCellsInfo(cells, layout),
35
- [cells, layout],
36
- );
33
+ const { slideCells, skippedIds, noOutputIds, slideTypes, startCellIndex } =
34
+ useMemo(() => computeSlideCellsInfo(cells, layout), [cells, layout]);
37
35
 
38
36
  const activeSlideIndex = activeCellId
39
- ? cellsWithOutput.findIndex((c) => c.id === activeCellId)
37
+ ? slideCells.findIndex((c) => c.id === activeCellId)
40
38
  : startCellIndex;
41
39
  const resolvedIndex =
42
40
  activeSlideIndex === -1 ? startCellIndex : activeSlideIndex;
43
41
 
44
42
  const handleSlideChange = useEvent((index: number) => {
45
- const cell = cellsWithOutput[index];
43
+ const cell = slideCells[index];
46
44
  if (cell) {
47
45
  setActiveCellId(cell.id);
48
46
  }
@@ -50,9 +48,10 @@ export const SlidesLayoutRenderer: React.FC<Props> = ({
50
48
 
51
49
  const slides = (
52
50
  <LazySlidesComponent
53
- cellsWithOutput={cellsWithOutput}
51
+ slideCells={slideCells}
54
52
  layout={layout}
55
53
  setLayout={setLayout}
54
+ noOutputIds={noOutputIds}
56
55
  activeIndex={resolvedIndex}
57
56
  onSlideChange={handleSlideChange}
58
57
  configWidth={280}
@@ -85,13 +84,12 @@ export const SlidesLayoutRenderer: React.FC<Props> = ({
85
84
  return (
86
85
  <div className="flex-1 pr-18 pb-2 flex flex-row gap-2 min-h-0">
87
86
  <SlidesMinimap
88
- cells={cellsWithOutput}
87
+ cells={slideCells}
89
88
  thumbnailWidth={220}
90
89
  canReorder={!isMultiColumn}
91
- activeCellId={
92
- activeCellId ?? cellsWithOutput[startCellIndex]?.id ?? null
93
- }
90
+ activeCellId={activeCellId ?? slideCells[startCellIndex]?.id ?? null}
94
91
  skippedIds={skippedIds}
92
+ noOutputIds={noOutputIds}
95
93
  slideTypes={slideTypes}
96
94
  onSlideClick={handleSlideChange}
97
95
  />
@@ -66,6 +66,7 @@ interface SlideThumbnailCardProps extends React.HTMLAttributes<HTMLDivElement> {
66
66
  isActiveDragSource?: boolean;
67
67
  isOverlay?: boolean;
68
68
  isVisible?: boolean;
69
+ isNoOutput?: boolean;
69
70
  slideType?: SlideType;
70
71
  ref?: React.Ref<HTMLDivElement>;
71
72
  }
@@ -77,6 +78,7 @@ interface SlideThumbnailRowProps extends React.ButtonHTMLAttributes<HTMLButtonEl
77
78
  dropIndicator?: DropPosition | null;
78
79
  isActiveDragSource?: boolean;
79
80
  isVisible?: boolean;
81
+ isNoOutput?: boolean;
80
82
  slideType?: SlideType;
81
83
  ref?: React.Ref<HTMLButtonElement>;
82
84
  }
@@ -86,15 +88,23 @@ interface SlidesMinimapProps {
86
88
  thumbnailWidth: number;
87
89
  canReorder: boolean;
88
90
  activeCellId: CellId | null;
89
- // Set of cell ids that are marked `skip` in the slides layout.
91
+ // Set of cell ids that should be visually treated like skipped entries.
90
92
  skippedIds?: ReadonlySet<CellId>;
93
+ // Set of cell ids that currently have no rendered output.
94
+ noOutputIds?: ReadonlySet<CellId>;
91
95
  slideTypes?: ReadonlyMap<CellId, SlideType>;
92
96
  onSlideClick: (index: number) => void;
93
97
  }
94
98
 
99
+ interface ThumbnailVisual {
100
+ label: string;
101
+ description: string;
102
+ Icon?: LucideIcon;
103
+ }
104
+
95
105
  function getSlideTypeVisual(
96
106
  slideType: SlideType | undefined,
97
- ): { label: string; description: string; Icon: LucideIcon } | null {
107
+ ): ThumbnailVisual | null {
98
108
  if (!slideType || slideType === "slide") {
99
109
  return null;
100
110
  }
@@ -102,8 +112,13 @@ function getSlideTypeVisual(
102
112
  return { label, description, Icon };
103
113
  }
104
114
 
115
+ const NO_OUTPUT_VISUAL: ThumbnailVisual = {
116
+ label: "No output",
117
+ description: "Hidden because this cell has no output.",
118
+ };
119
+
105
120
  const SLIDE_ASPECT_RATIO = 16 / 9;
106
- const SLIDE_BASE_WIDTH = 960;
121
+ const SLIDE_BASE_WIDTH = 520;
107
122
 
108
123
  function computeThumbnailDimensions(width: number): ThumbnailDimensions {
109
124
  return {
@@ -196,6 +211,7 @@ export const SlidesMinimap = ({
196
211
  canReorder,
197
212
  activeCellId,
198
213
  skippedIds,
214
+ noOutputIds,
199
215
  slideTypes,
200
216
  onSlideClick,
201
217
  }: SlidesMinimapProps) => {
@@ -288,6 +304,7 @@ export const SlidesMinimap = ({
288
304
  dimensions={dimensions}
289
305
  isActiveSlide={cell.id === activeCellId}
290
306
  isVisible={visibleIds.has(cell.id)}
307
+ isNoOutput={noOutputIds?.has(cell.id)}
291
308
  slideType={resolveSlideType({
292
309
  cellId: cell.id,
293
310
  slideTypes,
@@ -325,6 +342,7 @@ export const SlidesMinimap = ({
325
342
  isActive={activeId === cell.id}
326
343
  isActiveSlide={cell.id === activeCellId}
327
344
  isVisible={visibleIds.has(cell.id)}
345
+ isNoOutput={noOutputIds?.has(cell.id)}
328
346
  slideType={resolveSlideType({
329
347
  cellId: cell.id,
330
348
  slideTypes,
@@ -347,6 +365,7 @@ export const SlidesMinimap = ({
347
365
  dimensions={dimensions}
348
366
  isOverlay={true}
349
367
  isActiveDragSource={true}
368
+ isNoOutput={noOutputIds?.has(activeCell.id)}
350
369
  />
351
370
  )}
352
371
  </DragOverlay>
@@ -378,6 +397,7 @@ interface SortableSlideThumbnailProps {
378
397
  isActive: boolean;
379
398
  isActiveSlide?: boolean;
380
399
  isVisible?: boolean;
400
+ isNoOutput?: boolean;
381
401
  slideType?: SlideType;
382
402
  onClick?: () => void;
383
403
  }
@@ -389,6 +409,7 @@ const SortableSlideThumbnail = ({
389
409
  isActive,
390
410
  isActiveSlide,
391
411
  isVisible,
412
+ isNoOutput,
392
413
  slideType,
393
414
  onClick,
394
415
  }: SortableSlideThumbnailProps) => {
@@ -405,6 +426,7 @@ const SortableSlideThumbnail = ({
405
426
  isActiveDragSource={isActive}
406
427
  isActiveSlide={isActiveSlide}
407
428
  isVisible={isVisible}
429
+ isNoOutput={isNoOutput}
408
430
  slideType={slideType}
409
431
  onClick={onClick}
410
432
  {...attributes}
@@ -422,6 +444,7 @@ const SlideThumbnailRow = ({
422
444
  isActiveSlide = false,
423
445
  isActiveDragSource = false,
424
446
  isVisible,
447
+ isNoOutput,
425
448
  slideType,
426
449
  onClick,
427
450
  ref,
@@ -462,6 +485,7 @@ const SlideThumbnailRow = ({
462
485
  isActiveSlide={isActiveSlide}
463
486
  isActiveDragSource={isActiveDragSource}
464
487
  isVisible={isVisible}
488
+ isNoOutput={isNoOutput}
465
489
  slideType={slideType}
466
490
  />
467
491
  </button>
@@ -477,13 +501,14 @@ const SlideThumbnailCard = ({
477
501
  isActiveDragSource = false,
478
502
  isOverlay = false,
479
503
  isVisible = false,
504
+ isNoOutput = false,
480
505
  slideType,
481
506
  ref,
482
507
  ...props
483
508
  }: SlideThumbnailCardProps) => {
484
509
  const { width, height, scale } = dimensions;
485
- const visual = getSlideTypeVisual(slideType);
486
- const isSkipped = slideType === "skip";
510
+ const visual = isNoOutput ? NO_OUTPUT_VISUAL : getSlideTypeVisual(slideType);
511
+ const isSkipped = isNoOutput || slideType === "skip";
487
512
 
488
513
  const outerStyle: React.CSSProperties = {
489
514
  width,
@@ -525,16 +550,20 @@ const SlideThumbnailCard = ({
525
550
  height: height / scale,
526
551
  }}
527
552
  >
528
- <Slide cellId={cell.id} status={cell.status} output={cell.output} />
553
+ {isNoOutput ? (
554
+ <MiniCodePreview code={cell.code} />
555
+ ) : (
556
+ <Slide cellId={cell.id} status={cell.status} output={cell.output} />
557
+ )}
529
558
  </div>
530
559
  )}
531
560
  {isSkipped && (
532
561
  <div
533
- className="absolute inset-0 bg-muted/60 pointer-events-none"
562
+ className="absolute inset-0 bg-muted/50 pointer-events-none"
534
563
  aria-hidden={true}
535
564
  />
536
565
  )}
537
- {visual && (
566
+ {visual?.Icon && (
538
567
  <Tooltip
539
568
  content={
540
569
  <span className="text-xs opacity-80">{visual.description}</span>
@@ -550,7 +579,6 @@ const SlideThumbnailCard = ({
550
579
  aria-label={visual.label}
551
580
  >
552
581
  <visual.Icon className="h-3.5 w-3.5" />
553
- {/* <span>{visual.label}</span> */}
554
582
  </span>
555
583
  </Tooltip>
556
584
  )}
@@ -558,6 +586,14 @@ const SlideThumbnailCard = ({
558
586
  );
559
587
  };
560
588
 
589
+ const MiniCodePreview = ({ code }: { code: string }) => {
590
+ return (
591
+ <pre className="my-auto w-full overflow-hidden whitespace-pre-wrap wrap-break-word text-lg">
592
+ {code}
593
+ </pre>
594
+ );
595
+ };
596
+
561
597
  function projectDropTarget(
562
598
  event: DragMoveEvent | DragOverEvent,
563
599
  ): ProjectedDropTarget | null {
@@ -221,22 +221,50 @@ const SubslideView = ({
221
221
  );
222
222
  };
223
223
 
224
+ const ParkedPreviewContent = ({
225
+ cell,
226
+ isNoOutputPreview,
227
+ isEditable,
228
+ codeShown,
229
+ }: {
230
+ cell: RuntimeCell;
231
+ isNoOutputPreview: boolean;
232
+ isEditable: boolean;
233
+ codeShown: boolean;
234
+ }) => {
235
+ if (isNoOutputPreview && isEditable) {
236
+ return <SlideCellView cell={cell} />;
237
+ }
238
+ if (isNoOutputPreview && codeShown) {
239
+ return <SlideCellReadOnlyView cell={cell} />;
240
+ }
241
+ return (
242
+ <CellOutputSlide
243
+ cellId={cell.id}
244
+ status={cell.status}
245
+ output={cell.output}
246
+ />
247
+ );
248
+ };
249
+
224
250
  // There is an upstream react bug in dev mode (https://github.com/facebook/react/issues/34840)
225
251
  // Uncaught SecurityError: Failed to read a named property '$$typeof' from 'Window'
226
252
  // Happens with cells containing iframes / external content
227
253
  const RevealSlidesComponent = ({
228
- cellsWithOutput,
254
+ slideCells,
229
255
  layout,
230
256
  setLayout,
257
+ noOutputIds,
231
258
  activeIndex,
232
259
  onSlideChange,
233
260
  mode,
234
261
  configWidth, // px
235
262
  isEditable = false,
236
263
  }: {
237
- cellsWithOutput: RuntimeCell[];
264
+ slideCells: RuntimeCell[];
238
265
  layout: SlidesLayout;
239
266
  setLayout: (layout: SlidesLayout) => void;
267
+ noOutputIds: ReadonlySet<CellId>;
240
268
  activeIndex?: number;
241
269
  onSlideChange?: (index: number) => void;
242
270
  mode: AppMode;
@@ -255,42 +283,46 @@ const RevealSlidesComponent = ({
255
283
  );
256
284
 
257
285
  const [showCode, setShowCode] = useState(false);
258
- const codeAvailable = useNotebookCodeAvailable(cellsWithOutput);
286
+ const codeAvailable = useNotebookCodeAvailable(slideCells);
259
287
  const codeToggleEnabled = !isIslands() && codeAvailable;
260
288
  const codeShown = codeToggleEnabled && showCode;
261
289
 
262
- const activeCell =
263
- activeIndex != null ? cellsWithOutput[activeIndex] : undefined;
290
+ const activeCell = activeIndex != null ? slideCells[activeIndex] : undefined;
264
291
  // Fall back to the first cell while the deck settles on an initial slide.
265
292
  // Still `undefined` when the deck is empty (handled below).
266
- const activeConfigCell = activeCell ?? cellsWithOutput.at(0);
293
+ const activeConfigCell = activeCell ?? slideCells.at(0);
267
294
 
268
295
  const composition = useMemo(
269
296
  () =>
270
297
  composeSlides({
271
- cells: cellsWithOutput,
298
+ cells: slideCells,
272
299
  getType: (cell) =>
273
- layout.cells.get(cell.id)?.type ?? DEFAULT_SLIDE_TYPE,
300
+ noOutputIds.has(cell.id)
301
+ ? "skip"
302
+ : (layout.cells.get(cell.id)?.type ?? DEFAULT_SLIDE_TYPE),
274
303
  }),
275
- [cellsWithOutput, layout.cells],
304
+ [slideCells, noOutputIds, layout.cells],
276
305
  );
277
306
 
278
- // Skip cells aren't part of the composed deck. When one is selected in the
279
- // minimap we render a preview over the deck and park reveal on a neighboring
280
- // real slide; keyboard nav while parked is handled below.
281
- const skippedPreviewCell =
282
- activeCell && layout.cells.get(activeCell.id)?.type === "skip"
283
- ? activeCell
284
- : null;
307
+ // Skipped and output-less cells aren't part of the composed deck. When one is
308
+ // selected in the minimap we render a preview over the deck and park reveal on
309
+ // a neighboring real slide; keyboard nav while parked is handled below.
310
+ const activeCellSlideType = activeCell
311
+ ? layout.cells.get(activeCell.id)?.type
312
+ : undefined;
313
+ const isNoOutputPreview =
314
+ activeCell != null && noOutputIds.has(activeCell.id);
315
+ const isParkedPreview = activeCellSlideType === "skip" || isNoOutputPreview;
316
+ const parkedPreviewCell = isParkedPreview ? activeCell : null;
285
317
 
286
318
  const { cellToTarget, targetToCellIndex } = useMemo(
287
319
  () =>
288
320
  buildSlideIndices({
289
321
  composition,
290
- cells: cellsWithOutput,
322
+ cells: slideCells,
291
323
  getId: (c) => c.id,
292
324
  }),
293
- [composition, cellsWithOutput],
325
+ [composition, slideCells],
294
326
  );
295
327
 
296
328
  const deckTransition = layout.deck?.transition ?? DEFAULT_DECK_TRANSITION;
@@ -322,7 +354,7 @@ const RevealSlidesComponent = ({
322
354
  const navigateDeckToActiveCell = useEvent((deck: RevealApi) => {
323
355
  const target = resolveDeckNavigationTarget({
324
356
  activeIndex,
325
- cells: cellsWithOutput,
357
+ cells: slideCells,
326
358
  cellToTarget,
327
359
  getId: (cell) => cell.id,
328
360
  });
@@ -340,7 +372,7 @@ const RevealSlidesComponent = ({
340
372
  return;
341
373
  }
342
374
  navigateDeckToActiveCell(deck);
343
- }, [activeIndex, cellToTarget, cellsWithOutput, navigateDeckToActiveCell]);
375
+ }, [activeIndex, cellToTarget, slideCells, navigateDeckToActiveCell]);
344
376
 
345
377
  // Toggling code (re)mounts a CodeMirror editor on the active slide. Defer
346
378
  // the state update so the button/keypress paints first and the heavier mount
@@ -380,12 +412,12 @@ const RevealSlidesComponent = ({
380
412
  return { h: target.h, v: target.v };
381
413
  }, [activeCell, cellToTarget]);
382
414
 
383
- // Forward the deck's current cell to the parent, except while a skipped
415
+ // Forward the deck's current cell to the parent, except while a parked
384
416
  // preview is parked: every reveal.js event during that window is an echo
385
417
  // of the programmatic park (possibly with transient indices), so ignoring
386
- // them keeps `activeCellId` pinned on the skipped cell.
418
+ // them keeps `activeCellId` pinned on the minimap cell.
387
419
  const reportCurrentCell = useEvent(() => {
388
- if (skippedPreviewCell != null) {
420
+ if (parkedPreviewCell != null) {
389
421
  return;
390
422
  }
391
423
  const deck = deckRef.current;
@@ -401,10 +433,10 @@ const RevealSlidesComponent = ({
401
433
  }
402
434
  });
403
435
 
404
- // While parked on a skipped preview, step through minimap order instead of
436
+ // While parked on a preview, step through minimap order instead of
405
437
  // letting reveal.js advance from the parked slide the user can't see.
406
438
  const handleParkedNavKey = useEvent((event: KeyboardEvent) => {
407
- if (!skippedPreviewCell || activeIndex == null) {
439
+ if (!parkedPreviewCell || activeIndex == null) {
408
440
  return;
409
441
  }
410
442
  if (Events.fromInput(event)) {
@@ -418,7 +450,7 @@ const RevealSlidesComponent = ({
418
450
  event.preventDefault();
419
451
  event.stopPropagation();
420
452
  const nextIndex = activeIndex + direction;
421
- if (nextIndex < 0 || nextIndex >= cellsWithOutput.length) {
453
+ if (nextIndex < 0 || nextIndex >= slideCells.length) {
422
454
  return;
423
455
  }
424
456
  onSlideChange?.(nextIndex);
@@ -431,6 +463,10 @@ const RevealSlidesComponent = ({
431
463
 
432
464
  useEventListener(document, "keydown", handleParkedNavKey, { capture: true });
433
465
 
466
+ const parkedPreviewLabel = isNoOutputPreview
467
+ ? "Hidden as there is no output"
468
+ : "Skipped in presentation";
469
+
434
470
  const slideArea = (
435
471
  <div
436
472
  ref={containerRef}
@@ -480,21 +516,30 @@ const RevealSlidesComponent = ({
480
516
  );
481
517
  })}
482
518
  </Deck>
483
- {skippedPreviewCell && (
519
+ {parkedPreviewCell && (
484
520
  <div
521
+ key={parkedPreviewCell.id}
485
522
  className="absolute inset-0 z-10 border rounded bg-background flex flex-col overflow-hidden"
486
- aria-label="Skipped in presentation"
523
+ aria-label={parkedPreviewLabel}
487
524
  >
488
525
  <div className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground border-b bg-muted/40">
489
526
  <EyeOffIcon className="h-3.5 w-3.5" />
490
- <span>Skipped in presentation</span>
527
+ <span>{parkedPreviewLabel}</span>
491
528
  </div>
492
529
  <div className="flex-1 overflow-auto flex">
493
- <div className="mo-slide-content" style={{ margin: "auto 20px" }}>
494
- <CellOutputSlide
495
- cellId={skippedPreviewCell.id}
496
- status={skippedPreviewCell.status}
497
- output={skippedPreviewCell.output}
530
+ <div
531
+ className={
532
+ isNoOutputPreview && (isEditable || codeShown)
533
+ ? "mo-slide-content flex flex-col gap-3"
534
+ : "mo-slide-content"
535
+ }
536
+ style={{ margin: "auto 20px" }}
537
+ >
538
+ <ParkedPreviewContent
539
+ cell={parkedPreviewCell}
540
+ isNoOutputPreview={isNoOutputPreview}
541
+ isEditable={isEditable}
542
+ codeShown={codeShown}
498
543
  />
499
544
  </div>
500
545
  </div>