@marimo-team/islands 0.23.10-dev25 → 0.23.10-dev26

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.
@@ -27,6 +27,7 @@ import "./reveal-slides.css";
27
27
  import type {
28
28
  SlideConfig,
29
29
  SlidesLayout,
30
+ SlideType,
30
31
  } from "../editor/renderers/slides-layout/types";
31
32
  import {
32
33
  buildSlideIndices,
@@ -156,14 +157,148 @@ const NotesAside = ({ text }: { text: string }) => {
156
157
  );
157
158
  };
158
159
 
160
+ /**
161
+ * Resolve whether a slide cell shows its source instead of its output.
162
+ *
163
+ * Code is shown when either the cell's persisted `showCode` config is set or
164
+ * the keyboard toggle `C` override is active for it (logical OR).
165
+ */
166
+ export function shouldShowCode(options: {
167
+ cells: ReadonlyMap<CellId, SlideConfig>;
168
+ cellId: CellId | undefined;
169
+ showCodeOverrides: ReadonlySet<CellId>;
170
+ codeToggleEnabled: boolean;
171
+ }): boolean {
172
+ const { cells, cellId, showCodeOverrides, codeToggleEnabled } = options;
173
+ if (cellId == null || !codeToggleEnabled) {
174
+ return false;
175
+ }
176
+ const configured = cells.get(cellId)?.showCode ?? false;
177
+ return configured || showCodeOverrides.has(cellId);
178
+ }
179
+
180
+ /**
181
+ * The slide type a cell takes *in the composed deck*. Cells without output and
182
+ * the cell currently held in the parked edit overlay are dropped (`"skip"`) so
183
+ * they aren't mounted a second time in the deck — the overlay renders them
184
+ * instead. Everything else uses its configured type, defaulting to a slide.
185
+ */
186
+ export function deckSlideType(options: {
187
+ cell: RuntimeCell;
188
+ noOutputIds: ReadonlySet<CellId>;
189
+ heldEditCellId: CellId | null;
190
+ slideConfigs: ReadonlyMap<CellId, SlideConfig>;
191
+ }): SlideType {
192
+ const { cell, noOutputIds, heldEditCellId, slideConfigs } = options;
193
+ if (noOutputIds.has(cell.id) || cell.id === heldEditCellId) {
194
+ return "skip";
195
+ }
196
+ return slideConfigs.get(cell.id)?.type ?? DEFAULT_SLIDE_TYPE;
197
+ }
198
+
199
+ /**
200
+ * Tracks the cell pinned in the parked overlay (rendered over the deck for
201
+ * skipped / output-less cells, and during in-progress edits).
202
+ *
203
+ * A brand-new (or output-less) cell is edited in the parked overlay, which
204
+ * lives outside reveal's slide DOM. The moment it first produces output it
205
+ * would normally jump to its composed slide — a different React subtree that
206
+ * reveal also re-syncs/transitions — tearing down the editor and dropping
207
+ * focus mid-edit (e.g. typing in a new markdown cell). To avoid that we *hold*
208
+ * the cell in the overlay even after it gains output, and only let it settle
209
+ * into the deck once the user navigates to a different cell.
210
+ *
211
+ * The hold is keyed off the active cell rather than DOM focus on purpose: the
212
+ * slide editor doesn't participate in the global cell-focus state, and the
213
+ * active cell only changes when the user moves in the minimap — exactly when
214
+ * we want to release the hold.
215
+ *
216
+ * Returns:
217
+ * - `parkedPreviewCell`: the cell to render in the overlay
218
+ * - `isHeldEdit`: whether the cell is held in the overlay
219
+ * - `isNoOutputPreview`: whether the cell is output-less
220
+ * - `heldEditCellId`: the id of the cell that is held in the overlay
221
+ */
222
+ export function useParkedPreview(options: {
223
+ activeCell: RuntimeCell | undefined;
224
+ slideConfigs: ReadonlyMap<CellId, SlideConfig>;
225
+ noOutputIds: ReadonlySet<CellId>;
226
+ }): {
227
+ parkedPreviewCell: RuntimeCell | null;
228
+ isHeldEdit: boolean;
229
+ isNoOutputPreview: boolean;
230
+ heldEditCellId: CellId | null;
231
+ heldShowsCode: boolean;
232
+ toggleHeldShowsCode: () => void;
233
+ } {
234
+ const { activeCell, slideConfigs, noOutputIds } = options;
235
+ const activeCellId = activeCell?.id ?? null;
236
+ const isNoOutputPreview =
237
+ activeCell != null && noOutputIds.has(activeCell.id);
238
+ const isSkippedPreview =
239
+ activeCell != null && slideConfigs.get(activeCell.id)?.type === "skip";
240
+ // Genuinely parked: skipped in the deck, or no output to compose yet.
241
+ const baseParked = isSkippedPreview || isNoOutputPreview;
242
+
243
+ // The cell pinned in the overlay, tracked alongside the active cell it was
244
+ // armed against so we can release it exactly when the active cell changes.
245
+ const [held, setHeld] = useState<{
246
+ activeCellId: CellId | null;
247
+ cellId: CellId | null;
248
+ }>({ activeCellId, cellId: null });
249
+
250
+ let heldCellId = held.cellId;
251
+ if (held.activeCellId !== activeCellId) {
252
+ // Active cell changed: drop any prior hold, arming a fresh one only while
253
+ // the new cell has no output yet (skipped cells park via `baseParked`).
254
+ heldCellId = isNoOutputPreview ? activeCellId : null;
255
+ setHeld({ activeCellId, cellId: heldCellId });
256
+ } else if (isNoOutputPreview && heldCellId !== activeCellId) {
257
+ // Same active cell, still output-less: (re)arm the hold.
258
+ heldCellId = activeCellId;
259
+ setHeld({ activeCellId, cellId: heldCellId });
260
+ }
261
+
262
+ const isHeldEdit =
263
+ !baseParked && activeCellId != null && heldCellId === activeCellId;
264
+ // Keep the held cell out of the composed deck so its editor isn't mounted a
265
+ // second time (the overlay already renders it); it rejoins once released.
266
+ const heldEditCellId = isHeldEdit ? heldCellId : null;
267
+
268
+ // Code visibility for the held overlay. Defaults to showing the editor so it
269
+ // survives the no-output -> output transition mid-edit; the `C` toggle can
270
+ // hide it on demand.
271
+ const [heldShow, setHeldShow] = useState<{
272
+ cellId: CellId | null;
273
+ show: boolean;
274
+ }>({ cellId: heldEditCellId, show: true });
275
+ let heldShowsCode = heldShow.show;
276
+ if (heldShow.cellId !== heldEditCellId) {
277
+ heldShowsCode = true;
278
+ setHeldShow({ cellId: heldEditCellId, show: true });
279
+ }
280
+ const toggleHeldShowsCode = useEvent(() =>
281
+ setHeldShow((prev) => ({ ...prev, show: !prev.show })),
282
+ );
283
+
284
+ return {
285
+ parkedPreviewCell: baseParked || isHeldEdit ? (activeCell ?? null) : null,
286
+ isHeldEdit,
287
+ isNoOutputPreview,
288
+ heldEditCellId,
289
+ heldShowsCode,
290
+ toggleHeldShowsCode,
291
+ };
292
+ }
293
+
159
294
  const SubslideView = ({
160
295
  subslide,
161
- showCode,
296
+ resolveShowCode,
162
297
  isEditable,
163
298
  slideConfigs,
164
299
  }: {
165
300
  subslide: ComposedSubslide<RuntimeCell>;
166
- showCode: boolean;
301
+ resolveShowCode: (cellId: CellId) => boolean;
167
302
  isEditable: boolean;
168
303
  slideConfigs: ReadonlyMap<CellId, SlideConfig>;
169
304
  }) => {
@@ -172,12 +307,16 @@ const SubslideView = ({
172
307
  slideConfigs,
173
308
  );
174
309
 
310
+ const anyCodeShown = subslide.blocks.some((block) =>
311
+ block.cells.some((cell) => resolveShowCode(cell.id)),
312
+ );
313
+
175
314
  return (
176
315
  <Slide>
177
316
  <div className="h-full w-full overflow-auto flex">
178
317
  <div
179
318
  className={
180
- showCode
319
+ anyCodeShown
181
320
  ? "mo-slide-content flex flex-col gap-3"
182
321
  : "mo-slide-content"
183
322
  }
@@ -187,7 +326,7 @@ const SubslideView = ({
187
326
  >
188
327
  {subslide.blocks.map((block, i) => {
189
328
  const rendered = block.cells.map((cell) => {
190
- if (!showCode) {
329
+ if (!resolveShowCode(cell.id)) {
191
330
  return (
192
331
  <CellOutputSlide
193
332
  key={cell.id}
@@ -222,22 +361,36 @@ const SubslideView = ({
222
361
  );
223
362
  };
224
363
 
364
+ /**
365
+ * Whether the parked overlay renders the cell's *source* instead of its output.
366
+ */
367
+ export function parkedRendersSource(options: {
368
+ isNoOutputPreview: boolean;
369
+ isEditable: boolean;
370
+ showCode: boolean;
371
+ }): boolean {
372
+ const { isNoOutputPreview, isEditable, showCode } = options;
373
+ return isNoOutputPreview ? isEditable || showCode : showCode;
374
+ }
375
+
225
376
  const ParkedPreviewContent = ({
226
377
  cell,
227
378
  isNoOutputPreview,
228
379
  isEditable,
229
- codeShown,
380
+ showCode,
230
381
  }: {
231
382
  cell: RuntimeCell;
232
383
  isNoOutputPreview: boolean;
233
384
  isEditable: boolean;
234
- codeShown: boolean;
385
+ showCode: boolean;
235
386
  }) => {
236
- if (isNoOutputPreview && isEditable) {
237
- return <SlideCellView cell={cell} />;
238
- }
239
- if (isNoOutputPreview && codeShown) {
240
- return <SlideCellReadOnlyView cell={cell} />;
387
+ if (parkedRendersSource({ isNoOutputPreview, isEditable, showCode })) {
388
+ // Editable cells get the live editor; otherwise a read-only source view.
389
+ return isEditable ? (
390
+ <SlideCellView cell={cell} />
391
+ ) : (
392
+ <SlideCellReadOnlyView cell={cell} />
393
+ );
241
394
  }
242
395
  return (
243
396
  <CellOutputSlide
@@ -285,39 +438,68 @@ const RevealSlidesComponent = ({
285
438
  [kioskMode],
286
439
  );
287
440
 
288
- const [showCode, setShowCode] = useState(false);
441
+ // Store the state of the code toggle for each cell
442
+ // This acts like a 'peek' at the code.
443
+ const [showCodeOverrides, setShowCodeOverrides] = useState<
444
+ ReadonlySet<CellId>
445
+ >(() => new Set());
289
446
  const codeAvailable = useNotebookCodeAvailable(slideCells);
290
447
  const codeToggleEnabled = !isIslands() && codeAvailable;
291
- const codeShown = codeToggleEnabled && showCode;
292
448
 
293
449
  const activeCell = activeIndex != null ? slideCells[activeIndex] : undefined;
294
450
  // Fall back to the first cell while the deck settles on an initial slide.
295
451
  // Still `undefined` when the deck is empty (handled below).
296
452
  const activeConfigCell = activeCell ?? slideCells.at(0);
297
453
 
454
+ const {
455
+ parkedPreviewCell,
456
+ isHeldEdit,
457
+ isNoOutputPreview,
458
+ heldEditCellId,
459
+ heldShowsCode,
460
+ toggleHeldShowsCode,
461
+ } = useParkedPreview({
462
+ activeCell,
463
+ slideConfigs: layout.cells,
464
+ noOutputIds,
465
+ });
466
+
467
+ const resolveShowCode = (cellId: CellId | undefined): boolean =>
468
+ shouldShowCode({
469
+ cells: layout.cells,
470
+ cellId,
471
+ showCodeOverrides,
472
+ codeToggleEnabled,
473
+ });
474
+
475
+ // `C` and the toolbar button target the active slide's cell (the revealed
476
+ // fragment when stepping through a stack, otherwise the lead cell).
477
+ const cellIdToShowCode = activeCell?.id ?? activeConfigCell?.id;
478
+ const cellShowsCode = isHeldEdit
479
+ ? heldShowsCode
480
+ : resolveShowCode(cellIdToShowCode);
481
+
482
+ // A slide persisted with `showCode: true` always renders code
483
+ const codeAlwaysShown =
484
+ codeToggleEnabled &&
485
+ cellIdToShowCode != null &&
486
+ (layout.cells.get(cellIdToShowCode)?.showCode ?? false);
487
+
298
488
  const composition = useMemo(
299
489
  () =>
300
490
  composeSlides({
301
491
  cells: slideCells,
302
492
  getType: (cell) =>
303
- noOutputIds.has(cell.id)
304
- ? "skip"
305
- : (layout.cells.get(cell.id)?.type ?? DEFAULT_SLIDE_TYPE),
493
+ deckSlideType({
494
+ cell,
495
+ noOutputIds,
496
+ heldEditCellId,
497
+ slideConfigs: layout.cells,
498
+ }),
306
499
  }),
307
- [slideCells, noOutputIds, layout.cells],
500
+ [slideCells, noOutputIds, layout.cells, heldEditCellId],
308
501
  );
309
502
 
310
- // Skipped and output-less cells aren't part of the composed deck. When one is
311
- // selected in the minimap we render a preview over the deck and park reveal on
312
- // a neighboring real slide; keyboard nav while parked is handled below.
313
- const activeCellSlideType = activeCell
314
- ? layout.cells.get(activeCell.id)?.type
315
- : undefined;
316
- const isNoOutputPreview =
317
- activeCell != null && noOutputIds.has(activeCell.id);
318
- const isParkedPreview = activeCellSlideType === "skip" || isNoOutputPreview;
319
- const parkedPreviewCell = isParkedPreview ? activeCell : null;
320
-
321
503
  const { cellToTarget, targetToCellIndex } = useMemo(
322
504
  () =>
323
505
  buildSlideIndices({
@@ -339,6 +521,7 @@ const RevealSlidesComponent = ({
339
521
  url.searchParams.set("show-chrome", "false");
340
522
  return url.toString();
341
523
  }, []);
524
+
342
525
  const revealConfig: RevealConfig = useMemo(
343
526
  () => ({
344
527
  embedded: true,
@@ -381,14 +564,35 @@ const RevealSlidesComponent = ({
381
564
  // the state update so the button/keypress paints first and the heavier mount
382
565
  // can be interrupted by higher-priority work.
383
566
  const toggleShowCode = useEvent(() => {
384
- startTransition(() => setShowCode((value) => !value));
567
+ if (cellIdToShowCode == null || codeAlwaysShown) {
568
+ return;
569
+ }
570
+ if (isHeldEdit) {
571
+ toggleHeldShowsCode();
572
+ return;
573
+ }
574
+ startTransition(() =>
575
+ setShowCodeOverrides((prev) => {
576
+ const next = new Set(prev);
577
+ if (next.has(cellIdToShowCode)) {
578
+ next.delete(cellIdToShowCode);
579
+ } else {
580
+ next.add(cellIdToShowCode);
581
+ }
582
+ return next;
583
+ }),
584
+ );
385
585
  });
386
586
 
387
587
  const handleDeckReady = useEvent((deck: RevealApi) => {
388
588
  navigateDeckToActiveCell(deck);
389
589
  if (codeToggleEnabled) {
390
590
  deck.addKeyBinding(
391
- { keyCode: 67, key: "C", description: "Toggle code editor" },
591
+ {
592
+ keyCode: 67,
593
+ key: "C",
594
+ description: "Toggle code editor",
595
+ },
392
596
  toggleShowCode,
393
597
  );
394
598
  }
@@ -404,17 +608,6 @@ const RevealSlidesComponent = ({
404
608
  }
405
609
  });
406
610
 
407
- const activeSubslide = useMemo(() => {
408
- if (!activeCell) {
409
- return null;
410
- }
411
- const target = cellToTarget.get(activeCell.id);
412
- if (!target) {
413
- return null;
414
- }
415
- return { h: target.h, v: target.v };
416
- }, [activeCell, cellToTarget]);
417
-
418
611
  // Forward the deck's current cell to the parent, except while a parked
419
612
  // preview is parked: every reveal.js event during that window is an echo
420
613
  // of the programmatic park (possibly with transient indices), so ignoring
@@ -466,9 +659,22 @@ const RevealSlidesComponent = ({
466
659
 
467
660
  useEventListener(document, "keydown", handleParkedNavKey, { capture: true });
468
661
 
469
- const parkedPreviewLabel = isNoOutputPreview
470
- ? "Hidden as there is no output"
471
- : "Skipped in presentation";
662
+ // `isHeldEdit` means the cell already produces output and is only kept in the
663
+ // overlay so the editor survives the edit, so the parked banners don't apply.
664
+ const parkedPreviewLabel = isHeldEdit
665
+ ? null
666
+ : isNoOutputPreview
667
+ ? "Hidden as there is no output"
668
+ : "Skipped in presentation";
669
+
670
+ const parkedShowCode = isHeldEdit
671
+ ? heldShowsCode
672
+ : resolveShowCode(parkedPreviewCell?.id);
673
+ const parkedShowsSource = parkedRendersSource({
674
+ isNoOutputPreview,
675
+ isEditable,
676
+ showCode: parkedShowCode,
677
+ });
472
678
 
473
679
  const slideArea = (
474
680
  <div
@@ -488,13 +694,11 @@ const RevealSlidesComponent = ({
488
694
  >
489
695
  {composition.stacks.map((stack, h) => {
490
696
  if (stack.subslides.length === 1) {
491
- const isActive =
492
- activeSubslide?.h === h && activeSubslide?.v === 0;
493
697
  return (
494
698
  <SubslideView
495
699
  key={h}
496
700
  subslide={stack.subslides[0]}
497
- showCode={codeShown && isActive}
701
+ resolveShowCode={resolveShowCode}
498
702
  isEditable={isEditable}
499
703
  slideConfigs={layout.cells}
500
704
  />
@@ -503,13 +707,11 @@ const RevealSlidesComponent = ({
503
707
  return (
504
708
  <Stack key={h}>
505
709
  {stack.subslides.map((sub, v) => {
506
- const isActive =
507
- activeSubslide?.h === h && activeSubslide?.v === v;
508
710
  return (
509
711
  <SubslideView
510
712
  key={v}
511
713
  subslide={sub}
512
- showCode={codeShown && isActive}
714
+ resolveShowCode={resolveShowCode}
513
715
  isEditable={isEditable}
514
716
  slideConfigs={layout.cells}
515
717
  />
@@ -523,16 +725,18 @@ const RevealSlidesComponent = ({
523
725
  <div
524
726
  key={parkedPreviewCell.id}
525
727
  className="absolute inset-0 z-10 border rounded bg-background flex flex-col overflow-hidden"
526
- aria-label={parkedPreviewLabel}
728
+ aria-label={parkedPreviewLabel ?? undefined}
527
729
  >
528
- <div className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground border-b bg-muted/40">
529
- <EyeOffIcon className="h-3.5 w-3.5" />
530
- <span>{parkedPreviewLabel}</span>
531
- </div>
730
+ {parkedPreviewLabel && (
731
+ <div className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground border-b bg-muted/40">
732
+ <EyeOffIcon className="h-3.5 w-3.5" />
733
+ <span>{parkedPreviewLabel}</span>
734
+ </div>
735
+ )}
532
736
  <div className="flex-1 overflow-auto flex">
533
737
  <div
534
738
  className={
535
- isNoOutputPreview && (isEditable || codeShown)
739
+ parkedShowsSource
536
740
  ? "mo-slide-content flex flex-col gap-3"
537
741
  : "mo-slide-content"
538
742
  }
@@ -542,7 +746,7 @@ const RevealSlidesComponent = ({
542
746
  cell={parkedPreviewCell}
543
747
  isNoOutputPreview={isNoOutputPreview}
544
748
  isEditable={isEditable}
545
- codeShown={codeShown}
749
+ showCode={parkedShowCode}
546
750
  />
547
751
  </div>
548
752
  </div>
@@ -550,17 +754,35 @@ const RevealSlidesComponent = ({
550
754
  )}
551
755
  <div className="absolute top-2 right-2 z-20 opacity-0 group-hover:opacity-70 text-muted-foreground transition-opacity">
552
756
  {codeToggleEnabled && (
553
- <Tooltip content={codeShown ? "Hide code (C)" : "Show code (C)"}>
757
+ <Tooltip
758
+ content={
759
+ codeAlwaysShown
760
+ ? "Code is always shown for this slide"
761
+ : cellShowsCode
762
+ ? "Hide code (C)"
763
+ : "Show code (C)"
764
+ }
765
+ >
554
766
  <Button
555
767
  data-testid="marimo-plugin-slides-toggle-code"
556
768
  variant="ghost"
557
769
  size="icon"
770
+ // Stay hoverable (no `disabled` attr) so the tooltip can
771
+ // explain why the toggle is inert when code is pinned on.
558
772
  className={cn(
559
773
  "text-muted-foreground h-7 w-7",
560
- codeShown && "text-foreground bg-muted",
774
+ cellShowsCode && "text-foreground bg-muted",
775
+ codeAlwaysShown && "opacity-50 cursor-not-allowed",
561
776
  )}
562
- aria-pressed={codeShown}
563
- aria-label={codeShown ? "Hide code" : "Show code"}
777
+ aria-pressed={cellShowsCode}
778
+ aria-disabled={codeAlwaysShown}
779
+ aria-label={
780
+ codeAlwaysShown
781
+ ? "Code always shown"
782
+ : cellShowsCode
783
+ ? "Hide code"
784
+ : "Show code"
785
+ }
564
786
  onClick={toggleShowCode}
565
787
  >
566
788
  <CodeIcon className="h-4 w-4" />
@@ -3,14 +3,18 @@
3
3
  import { useMemo, useRef, useState } from "react";
4
4
  import type { EditorView } from "@codemirror/view";
5
5
  import { useAtomValue } from "jotai";
6
+ import useEvent from "react-use-event-hook";
6
7
  import { cellDomProps } from "@/components/editor/common";
7
8
  import { CellEditor } from "@/components/editor/cell/code/cell-editor";
9
+ import { LanguageToggles } from "@/components/editor/cell/code/language-toggle";
8
10
  import { CellStatusComponent } from "@/components/editor/cell/CellStatus";
9
11
  import { RunButton } from "@/components/editor/cell/RunButton";
10
12
  import { StopButton } from "@/components/editor/cell/StopButton";
11
13
  import { useRunCell } from "@/components/editor/cell/useRunCells";
12
14
  import { Slide as CellOutputSlide } from "@/components/slides/slide";
13
- import { useUserConfig } from "@/core/config/config";
15
+ import { maybeAddMarimoImport } from "@/core/cells/add-missing-import";
16
+ import { useCellActions } from "@/core/cells/cells";
17
+ import { autoInstantiateAtom, useUserConfig } from "@/core/config/config";
14
18
  import {
15
19
  cellNeedsRun,
16
20
  cellStatusClasses,
@@ -37,12 +41,24 @@ export const SlideCellView = ({ cell }: { cell: RuntimeCell }) => {
37
41
  const { theme } = useTheme();
38
42
  const runCell = useRunCell(cell.id);
39
43
  const connection = useAtomValue(connectionAtom);
44
+ const cellActions = useCellActions();
45
+ const autoInstantiate = useAtomValue(autoInstantiateAtom);
40
46
  const editorViewRef = useRef<EditorView | null>(null);
41
47
  const editorViewParentRef = useRef<HTMLDivElement | null>(null);
42
48
  const [languageAdapter, setLanguageAdapter] = useState<
43
49
  LanguageAdapterType | undefined
44
50
  >();
45
51
 
52
+ const afterToggleLanguage = useEvent(() => {
53
+ maybeAddMarimoImport({
54
+ autoInstantiate,
55
+ createNewCell: cellActions.createNewCell,
56
+ });
57
+ });
58
+
59
+ // Must be a stable identity: it feeds the editor's `extensions` memo
60
+ const showHiddenCode = useEvent(() => undefined);
61
+
46
62
  const cellOutputPosition = userConfig.display.cell_output;
47
63
  const hasOutput = cell.output != null;
48
64
 
@@ -97,6 +113,13 @@ export const SlideCellView = ({ cell }: { cell: RuntimeCell }) => {
97
113
  lastRunStartTimestamp={cell.lastRunStartTimestamp}
98
114
  uninstantiated={uninstantiated}
99
115
  />
116
+ <LanguageToggles
117
+ code={cell.code}
118
+ editorView={editorViewRef.current}
119
+ currentLanguageAdapter={languageAdapter}
120
+ onAfterToggle={afterToggleLanguage}
121
+ className="flex items-center gap-1"
122
+ />
100
123
  <div className="flex items-center shadow-none gap-1">
101
124
  <RunButton
102
125
  edited={cell.edited}
@@ -113,6 +136,7 @@ export const SlideCellView = ({ cell }: { cell: RuntimeCell }) => {
113
136
 
114
137
  const editor = (
115
138
  <div
139
+ tabIndex={-1}
116
140
  className={editorWrapperClassName}
117
141
  {...cellDomProps(cell.id, cell.name)}
118
142
  >
@@ -134,7 +158,7 @@ export const SlideCellView = ({ cell }: { cell: RuntimeCell }) => {
134
158
  hasOutput={hasOutput}
135
159
  // hide_code is intentionally overridden in the slide view; the editor
136
160
  // is unmounted entirely when the user toggles code off.
137
- showHiddenCode={() => undefined}
161
+ showHiddenCode={showHiddenCode}
138
162
  languageAdapter={languageAdapter}
139
163
  setLanguageAdapter={setLanguageAdapter}
140
164
  showLanguageToggles={false}
@@ -10,6 +10,7 @@ import {
10
10
  PanelRightOpenIcon,
11
11
  KeyboardIcon,
12
12
  } from "lucide-react";
13
+ import { Switch } from "@/components/ui/switch";
13
14
  import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
14
15
  import {
15
16
  Select,
@@ -196,13 +197,22 @@ const SlideConfigForm = ({
196
197
  setLayout: (layout: SlidesLayout) => void;
197
198
  cellId: CellId;
198
199
  }) => {
199
- const currentSlideType: SlideType =
200
- layout.cells.get(cellId)?.type ?? DEFAULT_SLIDE_TYPE;
200
+ const currentConfig = layout.cells.get(cellId);
201
+ const currentSlideType: SlideType = currentConfig?.type ?? DEFAULT_SLIDE_TYPE;
202
+ const showCode = currentConfig?.showCode ?? false;
201
203
 
202
204
  const handleSlideTypeChange = (value: SlideType) => {
203
- const existingConfig = layout.cells.get(cellId);
204
205
  const newCells = new Map(layout.cells);
205
- newCells.set(cellId, { ...existingConfig, type: value });
206
+ newCells.set(cellId, { ...currentConfig, type: value });
207
+ setLayout({
208
+ ...layout,
209
+ cells: newCells,
210
+ });
211
+ };
212
+
213
+ const handleShowCodeChange = (checked: boolean) => {
214
+ const newCells = new Map(layout.cells);
215
+ newCells.set(cellId, { ...currentConfig, showCode: checked });
206
216
  setLayout({
207
217
  ...layout,
208
218
  cells: newCells,
@@ -261,6 +271,18 @@ const SlideConfigForm = ({
261
271
  );
262
272
  })}
263
273
  </RadioGroup>
274
+ <div className="flex items-center gap-2">
275
+ <label htmlFor="slide-show-code" className="text-sm">
276
+ Show code
277
+ </label>
278
+ <Switch
279
+ id="slide-show-code"
280
+ aria-label="Show code"
281
+ checked={showCode}
282
+ onCheckedChange={handleShowCodeChange}
283
+ size="sm"
284
+ />
285
+ </div>
264
286
  </div>
265
287
  );
266
288
  };