@marimo-team/islands 0.23.10-dev3 → 0.23.10-dev30

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.
Files changed (165) hide show
  1. package/dist/{ConnectedDataExplorerComponent-CyV83R2m.js → ConnectedDataExplorerComponent-DmBropAy.js} +31 -31
  2. package/dist/{ErrorBoundary-rULOrC_p.js → ErrorBoundary-DpbaKVv7.js} +1 -1
  3. package/dist/{any-language-editor-DfdpyDv_.js → any-language-editor-DNmoSiWL.js} +20 -20
  4. package/dist/assets/__vite-browser-external-eshhtsgZ.js +1 -0
  5. package/dist/assets/worker-CC0Oul9k.js +73 -0
  6. package/dist/{chat-ui-C1tL1pML.js → chat-ui-D6oraHT2.js} +76 -76
  7. package/dist/{check-DTbrK0zt.js → check-BCaJeT-J.js} +1 -1
  8. package/dist/{code-visibility-DfnO0DcH.js → code-visibility-wR7WSQ4c.js} +2166 -1292
  9. package/dist/{copy-BuQpJEzp.js → copy-UqRYxiOg.js} +33 -33
  10. package/dist/dist-7QfXoMdB.js +5 -0
  11. package/dist/{dist-DgnE8F-r.js → dist-A2846XWO.js} +1 -1
  12. package/dist/dist-BEXXyZig.js +5 -0
  13. package/dist/{dist-B3pZ0Ab6.js → dist-BR_gyG9L.js} +3 -3
  14. package/dist/{dist-CcXxepx6.js → dist-BSAt6RhH.js} +27 -27
  15. package/dist/{dist-Bde4a2kU.js → dist-BY018Paw.js} +8 -8
  16. package/dist/dist-BYj57OV4.js +5 -0
  17. package/dist/{dist-CUCNs1ja.js → dist-BaoDKvdy.js} +2 -2
  18. package/dist/{dist-Cy1WxgBD.js → dist-Bf7SHuNp.js} +5 -5
  19. package/dist/{dist-Bz_sYWbr.js → dist-Bk75fBZA.js} +2 -2
  20. package/dist/dist-BlSvQzNr.js +5 -0
  21. package/dist/{dist-C5VC_yzu.js → dist-BzEzfugY.js} +1 -1
  22. package/dist/dist-CCBlxAgS.js +8 -0
  23. package/dist/dist-CIDTVIUf.js +5 -0
  24. package/dist/{dist-CLUtPrdy.js → dist-CIYBwstr.js} +1 -1
  25. package/dist/{dist-BotSqB48.js → dist-C_Y3oV3C.js} +12 -12
  26. package/dist/{dist-BTfv03uy.js → dist-CcWX6tmx.js} +2 -2
  27. package/dist/{dist-BhM8gdSO.js → dist-CoXAujgg.js} +4 -4
  28. package/dist/{dist-4j4c7bjm.js → dist-CpxNdDkw.js} +3 -3
  29. package/dist/dist-CqQyhAM8.js +8 -0
  30. package/dist/dist-CwRu2Xzh.js +5 -0
  31. package/dist/{dist-BcuoonNH.js → dist-CxJDU6Bh.js} +9 -9
  32. package/dist/{dist-DxvORzUR.js → dist-D-W5ny5a.js} +8 -8
  33. package/dist/dist-D8CDTVgf.js +6 -0
  34. package/dist/dist-D8DNB0nO.js +8 -0
  35. package/dist/dist-DL6N_q-A.js +5 -0
  36. package/dist/{dist-BbbIBDiQ.js → dist-DMjWuVs8.js} +1 -1
  37. package/dist/dist-DOFbNV_b.js +8 -0
  38. package/dist/dist-DPrYzMY0.js +6 -0
  39. package/dist/{dist-h2c8sZvT.js → dist-DZORgqKY.js} +1 -1
  40. package/dist/{dist-B3P2fFpz.js → dist-DZo4nSS0.js} +14 -14
  41. package/dist/{dist-D4CewLk6.js → dist-Dax--nl9.js} +1 -1
  42. package/dist/{dist-DRfcqpxJ.js → dist-DgGbNavJ.js} +2 -2
  43. package/dist/{dist-C1BYNeCR.js → dist-Dk6PV_d3.js} +10 -10
  44. package/dist/{dist-fQ0ViXGs.js → dist-Dv_Y15yk.js} +107 -107
  45. package/dist/{dist-Bfwsv11D.js → dist-DyyjKEYf.js} +2 -2
  46. package/dist/{dist-p2qyWijU.js → dist-GZXUmt0b.js} +2 -2
  47. package/dist/{dist-CLJWPTX2.js → dist-LTU8Hdvn.js} +3 -3
  48. package/dist/{dist-DqAWR3CS.js → dist-M9Vag9Y0.js} +20 -20
  49. package/dist/{dist-DNdhYsgW.js → dist-U4F-tbMs.js} +79 -62
  50. package/dist/{dist-RqXTaiir.js → dist-abid3KgM.js} +11 -11
  51. package/dist/dist-cdmMjgsn.js +5 -0
  52. package/dist/dist-hT4QzYX-.js +1247 -0
  53. package/dist/{dist-luvabDEB.js → dist-t9Kf7xqC.js} +2 -2
  54. package/dist/{error-banner-5bz0L9hS.js → error-banner-Cc0I3C9e.js} +1 -1
  55. package/dist/esm-BaH2eg5-.js +1171 -0
  56. package/dist/{esm-Duie8iU-.js → esm-ga2Bf3O2.js} +43 -43
  57. package/dist/{extends-BgdxCfYu.js → extends-D_hDsj6R.js} +4 -4
  58. package/dist/{formats-DHxc-FdY.js → formats-C4wO47tk.js} +1 -1
  59. package/dist/{glide-data-editor-BOmK9ETQ.js → glide-data-editor-Qhu8oCX-.js} +12 -12
  60. package/dist/{html-to-image-CNa5ok96.js → html-to-image-UEH5lFDZ.js} +2318 -2275
  61. package/dist/{input-_2sjvfne.js → input-CMYy4hzj.js} +187 -185
  62. package/dist/{label-LWtdw5i8.js → label-CC4ytI1X.js} +1 -1
  63. package/dist/main.js +6941 -6913
  64. package/dist/{mermaid-lXOw5Py9.js → mermaid-zuLgJ8J8.js} +4 -4
  65. package/dist/{process-output-DKr4f1di.js → process-output-CyMLTogj.js} +3 -3
  66. package/dist/{reveal-component-UdMnCK5U.js → reveal-component-BjnkUAZ9.js} +697 -619
  67. package/dist/{spec-B96zNUEA.js → spec-X7FwLJni.js} +4 -4
  68. package/dist/{strings-Bu3vlb6W.js → strings-J57tzLr3.js} +47 -46
  69. package/dist/style.css +1 -1
  70. package/dist/{toDate-x-WRDCH7.js → toDate-d8RCRrRd.js} +2 -2
  71. package/dist/{tooltip-C5FYOpQc.js → tooltip-DpcyNkQ2.js} +2 -2
  72. package/dist/{types-CVvp1fKr.js → types-ChtMFmZ2.js} +1 -1
  73. package/dist/{useAsyncData-iRgKDT5s.js → useAsyncData-PonK__yh.js} +1 -1
  74. package/dist/{useDateFormatter-BA4FCquG.js → useDateFormatter-QB-3MpYr.js} +2 -2
  75. package/dist/{useDeepCompareMemoize-CkQ57VS2.js → useDeepCompareMemoize-D3NGWke6.js} +1 -1
  76. package/dist/{useLifecycle-BBO9PIph.js → useLifecycle-00mO3OSS.js} +2 -2
  77. package/dist/{useTheme-DHIrRQOe.js → useTheme-DEhDzATN.js} +1 -1
  78. package/dist/{vega-component-Dq-SH463.js → vega-component-9h1ACS78.js} +8 -8
  79. package/dist/{zod-CoBiJ5v4.js → zod-aLSua2NL.js} +24 -23
  80. package/package.json +3 -3
  81. package/src/components/data-table/TableBottomBar.tsx +1 -15
  82. package/src/components/data-table/TableTopBar.tsx +8 -13
  83. package/src/components/data-table/__tests__/TableBottomBar.test.tsx +6 -12
  84. package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +227 -0
  85. package/src/components/data-table/__tests__/data-table.test.tsx +154 -12
  86. package/src/components/data-table/column-visibility-dropdown.tsx +204 -0
  87. package/src/components/data-table/data-table.tsx +1 -1
  88. package/src/components/data-table/filter-by-values-picker.tsx +39 -17
  89. package/src/components/data-table/filter-pills.tsx +1 -1
  90. package/src/components/data-table/hover-tooltip/__tests__/content.test.ts +60 -0
  91. package/src/components/data-table/hover-tooltip/content.ts +44 -0
  92. package/src/components/data-table/hover-tooltip/hover-tooltip.tsx +55 -0
  93. package/src/components/data-table/hover-tooltip/use-table-hover-tooltip.ts +159 -0
  94. package/src/components/data-table/renderers.tsx +27 -43
  95. package/src/components/datasources/__tests__/filter-empty.test.ts +183 -0
  96. package/src/components/datasources/datasources.tsx +92 -3
  97. package/src/components/editor/cell/cell-context-menu.tsx +15 -2
  98. package/src/components/editor/cell/code/language-toggle.tsx +7 -1
  99. package/src/components/editor/chrome/wrapper/app-chrome.tsx +97 -52
  100. package/src/components/editor/chrome/wrapper/lazy-panels.ts +91 -0
  101. package/src/components/editor/chrome/wrapper/sidebar.tsx +2 -0
  102. package/src/components/editor/documentation.css +35 -0
  103. package/src/components/editor/file-tree/file-explorer.tsx +8 -18
  104. package/src/components/editor/file-tree/tree-actions.tsx +46 -1
  105. package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +20 -0
  106. package/src/components/editor/renderers/slides-layout/types.ts +1 -0
  107. package/src/components/slides/__tests__/minimap-actions.test.tsx +166 -0
  108. package/src/components/slides/__tests__/reveal-component.test.ts +425 -0
  109. package/src/components/slides/minimap.tsx +127 -10
  110. package/src/components/slides/reveal-component.tsx +287 -61
  111. package/src/components/slides/slide-cell-view.tsx +26 -2
  112. package/src/components/slides/slide-form.tsx +26 -4
  113. package/src/components/storage/__tests__/storage-inspector.test.ts +53 -0
  114. package/src/components/storage/storage-inspector.tsx +68 -48
  115. package/src/components/ui/__tests__/use-toast.test.ts +75 -0
  116. package/src/components/ui/combobox.tsx +51 -32
  117. package/src/components/ui/reorderable-list.tsx +13 -0
  118. package/src/components/ui/select-core/__tests__/use-select-list.test.ts +294 -0
  119. package/src/components/ui/select-core/__tests__/utils.test.ts +222 -0
  120. package/src/components/ui/select-core/index.ts +16 -0
  121. package/src/components/ui/select-core/option-row.tsx +33 -0
  122. package/src/components/ui/select-core/render-slot.ts +20 -0
  123. package/src/components/ui/select-core/select-list.tsx +248 -0
  124. package/src/components/ui/select-core/types.ts +44 -0
  125. package/src/components/ui/select-core/use-select-list.ts +347 -0
  126. package/src/components/ui/select-core/utils.ts +121 -0
  127. package/src/components/ui/use-toast.ts +33 -13
  128. package/src/core/cells/__tests__/__snapshots__/cells.test.ts.snap +0 -28
  129. package/src/core/cells/__tests__/cell.test.ts +29 -2
  130. package/src/core/cells/cell.ts +5 -1
  131. package/src/core/codemirror/go-to-definition/commands.ts +4 -3
  132. package/src/core/codemirror/language/languages/python.ts +2 -0
  133. package/src/core/codemirror/language/languages/sql/utils.ts +3 -1
  134. package/src/core/codemirror/lsp/__tests__/markdown-renderer.test.ts +41 -0
  135. package/src/core/codemirror/lsp/markdown-renderer.ts +59 -0
  136. package/src/core/datasets/data-source-connections.ts +2 -0
  137. package/src/core/network/__tests__/requests-static.test.ts +30 -0
  138. package/src/core/network/requests-static.ts +14 -10
  139. package/src/core/wasm/worker/bootstrap.ts +12 -4
  140. package/src/plugins/impl/MultiselectPlugin.tsx +19 -142
  141. package/src/plugins/impl/SearchableSelect.tsx +16 -97
  142. package/src/plugins/impl/__tests__/DropdownPlugin.test.tsx +5 -2
  143. package/src/plugins/impl/__tests__/MultiSelectPlugin.test.ts +1 -1
  144. package/src/plugins/layout/DownloadPlugin.tsx +1 -1
  145. package/src/utils/lazy.ts +6 -1
  146. package/dist/assets/__vite-browser-external-Ci2ZQfXU.js +0 -1
  147. package/dist/assets/worker-ip3AI_sN.js +0 -73
  148. package/dist/dist-0Fif7jnk.js +0 -5
  149. package/dist/dist-B5h_9sHB.js +0 -6
  150. package/dist/dist-B9M6R5ye.js +0 -5
  151. package/dist/dist-BCt3tnck.js +0 -8
  152. package/dist/dist-BUIJwMwn.js +0 -8
  153. package/dist/dist-BpquMd3k.js +0 -5
  154. package/dist/dist-BzJsqYfz.js +0 -5
  155. package/dist/dist-CA5ELXAf.js +0 -6
  156. package/dist/dist-CLBRs6Uv.js +0 -5
  157. package/dist/dist-CStVCMbq.js +0 -5
  158. package/dist/dist-CZRIEY3Y.js +0 -8
  159. package/dist/dist-CuUHbFD0.js +0 -5
  160. package/dist/dist-DV7Iabxb.js +0 -8
  161. package/dist/dist-DhHh0jLg.js +0 -1247
  162. package/dist/dist-DuEeHMvL.js +0 -5
  163. package/dist/esm-BfhQmZjp.js +0 -1171
  164. package/src/plugins/impl/multiselectFilterFn.tsx +0 -22
  165. /package/src/components/{data-table → ui}/value-chips.tsx +0 -0
@@ -14,6 +14,7 @@ import { Deck, Fragment, Slide, Stack } from "@revealjs/react";
14
14
  import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
15
15
  import { Slide as CellOutputSlide } from "@/components/slides/slide";
16
16
  import { Button } from "@/components/ui/button";
17
+ import { useFullScreenElement } from "@/components/ui/fullscreen";
17
18
  import { Tooltip } from "@/components/ui/tooltip";
18
19
  import type { CellId } from "@/core/cells/ids";
19
20
  import type { RuntimeCell } from "@/core/cells/types";
@@ -26,6 +27,7 @@ import "./reveal-slides.css";
26
27
  import type {
27
28
  SlideConfig,
28
29
  SlidesLayout,
30
+ SlideType,
29
31
  } from "../editor/renderers/slides-layout/types";
30
32
  import {
31
33
  buildSlideIndices,
@@ -155,14 +157,148 @@ const NotesAside = ({ text }: { text: string }) => {
155
157
  );
156
158
  };
157
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
+
158
294
  const SubslideView = ({
159
295
  subslide,
160
- showCode,
296
+ resolveShowCode,
161
297
  isEditable,
162
298
  slideConfigs,
163
299
  }: {
164
300
  subslide: ComposedSubslide<RuntimeCell>;
165
- showCode: boolean;
301
+ resolveShowCode: (cellId: CellId) => boolean;
166
302
  isEditable: boolean;
167
303
  slideConfigs: ReadonlyMap<CellId, SlideConfig>;
168
304
  }) => {
@@ -171,12 +307,16 @@ const SubslideView = ({
171
307
  slideConfigs,
172
308
  );
173
309
 
310
+ const anyCodeShown = subslide.blocks.some((block) =>
311
+ block.cells.some((cell) => resolveShowCode(cell.id)),
312
+ );
313
+
174
314
  return (
175
315
  <Slide>
176
316
  <div className="h-full w-full overflow-auto flex">
177
317
  <div
178
318
  className={
179
- showCode
319
+ anyCodeShown
180
320
  ? "mo-slide-content flex flex-col gap-3"
181
321
  : "mo-slide-content"
182
322
  }
@@ -186,7 +326,7 @@ const SubslideView = ({
186
326
  >
187
327
  {subslide.blocks.map((block, i) => {
188
328
  const rendered = block.cells.map((cell) => {
189
- if (!showCode) {
329
+ if (!resolveShowCode(cell.id)) {
190
330
  return (
191
331
  <CellOutputSlide
192
332
  key={cell.id}
@@ -221,22 +361,36 @@ const SubslideView = ({
221
361
  );
222
362
  };
223
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
+
224
376
  const ParkedPreviewContent = ({
225
377
  cell,
226
378
  isNoOutputPreview,
227
379
  isEditable,
228
- codeShown,
380
+ showCode,
229
381
  }: {
230
382
  cell: RuntimeCell;
231
383
  isNoOutputPreview: boolean;
232
384
  isEditable: boolean;
233
- codeShown: boolean;
385
+ showCode: boolean;
234
386
  }) => {
235
- if (isNoOutputPreview && isEditable) {
236
- return <SlideCellView cell={cell} />;
237
- }
238
- if (isNoOutputPreview && codeShown) {
239
- 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
+ );
240
394
  }
241
395
  return (
242
396
  <CellOutputSlide
@@ -274,6 +428,8 @@ const RevealSlidesComponent = ({
274
428
  const containerRef = useRef<HTMLDivElement>(null);
275
429
  const deckRef = useRef<RevealApi | null>(null);
276
430
  const { width, height } = useSlideDimensions(containerRef);
431
+ const isFullscreen = useFullScreenElement() != null;
432
+
277
433
  // Skip the Notes plugin inside reveal's own speaker-view iframes so pressing
278
434
  // `S` there doesn't try to spawn another popup.
279
435
  const kioskMode = useAtomValue(kioskModeAtom);
@@ -282,39 +438,68 @@ const RevealSlidesComponent = ({
282
438
  [kioskMode],
283
439
  );
284
440
 
285
- 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());
286
446
  const codeAvailable = useNotebookCodeAvailable(slideCells);
287
447
  const codeToggleEnabled = !isIslands() && codeAvailable;
288
- const codeShown = codeToggleEnabled && showCode;
289
448
 
290
449
  const activeCell = activeIndex != null ? slideCells[activeIndex] : undefined;
291
450
  // Fall back to the first cell while the deck settles on an initial slide.
292
451
  // Still `undefined` when the deck is empty (handled below).
293
452
  const activeConfigCell = activeCell ?? slideCells.at(0);
294
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
+
295
488
  const composition = useMemo(
296
489
  () =>
297
490
  composeSlides({
298
491
  cells: slideCells,
299
492
  getType: (cell) =>
300
- noOutputIds.has(cell.id)
301
- ? "skip"
302
- : (layout.cells.get(cell.id)?.type ?? DEFAULT_SLIDE_TYPE),
493
+ deckSlideType({
494
+ cell,
495
+ noOutputIds,
496
+ heldEditCellId,
497
+ slideConfigs: layout.cells,
498
+ }),
303
499
  }),
304
- [slideCells, noOutputIds, layout.cells],
500
+ [slideCells, noOutputIds, layout.cells, heldEditCellId],
305
501
  );
306
502
 
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;
317
-
318
503
  const { cellToTarget, targetToCellIndex } = useMemo(
319
504
  () =>
320
505
  buildSlideIndices({
@@ -336,6 +521,7 @@ const RevealSlidesComponent = ({
336
521
  url.searchParams.set("show-chrome", "false");
337
522
  return url.toString();
338
523
  }, []);
524
+
339
525
  const revealConfig: RevealConfig = useMemo(
340
526
  () => ({
341
527
  embedded: true,
@@ -378,14 +564,35 @@ const RevealSlidesComponent = ({
378
564
  // the state update so the button/keypress paints first and the heavier mount
379
565
  // can be interrupted by higher-priority work.
380
566
  const toggleShowCode = useEvent(() => {
381
- 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
+ );
382
585
  });
383
586
 
384
587
  const handleDeckReady = useEvent((deck: RevealApi) => {
385
588
  navigateDeckToActiveCell(deck);
386
589
  if (codeToggleEnabled) {
387
590
  deck.addKeyBinding(
388
- { keyCode: 67, key: "C", description: "Toggle code editor" },
591
+ {
592
+ keyCode: 67,
593
+ key: "C",
594
+ description: "Toggle code editor",
595
+ },
389
596
  toggleShowCode,
390
597
  );
391
598
  }
@@ -401,17 +608,6 @@ const RevealSlidesComponent = ({
401
608
  }
402
609
  });
403
610
 
404
- const activeSubslide = useMemo(() => {
405
- if (!activeCell) {
406
- return null;
407
- }
408
- const target = cellToTarget.get(activeCell.id);
409
- if (!target) {
410
- return null;
411
- }
412
- return { h: target.h, v: target.v };
413
- }, [activeCell, cellToTarget]);
414
-
415
611
  // Forward the deck's current cell to the parent, except while a parked
416
612
  // preview is parked: every reveal.js event during that window is an echo
417
613
  // of the programmatic park (possibly with transient indices), so ignoring
@@ -463,9 +659,22 @@ const RevealSlidesComponent = ({
463
659
 
464
660
  useEventListener(document, "keydown", handleParkedNavKey, { capture: true });
465
661
 
466
- const parkedPreviewLabel = isNoOutputPreview
467
- ? "Hidden as there is no output"
468
- : "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
+ });
469
678
 
470
679
  const slideArea = (
471
680
  <div
@@ -485,13 +694,11 @@ const RevealSlidesComponent = ({
485
694
  >
486
695
  {composition.stacks.map((stack, h) => {
487
696
  if (stack.subslides.length === 1) {
488
- const isActive =
489
- activeSubslide?.h === h && activeSubslide?.v === 0;
490
697
  return (
491
698
  <SubslideView
492
699
  key={h}
493
700
  subslide={stack.subslides[0]}
494
- showCode={codeShown && isActive}
701
+ resolveShowCode={resolveShowCode}
495
702
  isEditable={isEditable}
496
703
  slideConfigs={layout.cells}
497
704
  />
@@ -500,13 +707,11 @@ const RevealSlidesComponent = ({
500
707
  return (
501
708
  <Stack key={h}>
502
709
  {stack.subslides.map((sub, v) => {
503
- const isActive =
504
- activeSubslide?.h === h && activeSubslide?.v === v;
505
710
  return (
506
711
  <SubslideView
507
712
  key={v}
508
713
  subslide={sub}
509
- showCode={codeShown && isActive}
714
+ resolveShowCode={resolveShowCode}
510
715
  isEditable={isEditable}
511
716
  slideConfigs={layout.cells}
512
717
  />
@@ -520,16 +725,18 @@ const RevealSlidesComponent = ({
520
725
  <div
521
726
  key={parkedPreviewCell.id}
522
727
  className="absolute inset-0 z-10 border rounded bg-background flex flex-col overflow-hidden"
523
- aria-label={parkedPreviewLabel}
728
+ aria-label={parkedPreviewLabel ?? undefined}
524
729
  >
525
- <div className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground border-b bg-muted/40">
526
- <EyeOffIcon className="h-3.5 w-3.5" />
527
- <span>{parkedPreviewLabel}</span>
528
- </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
+ )}
529
736
  <div className="flex-1 overflow-auto flex">
530
737
  <div
531
738
  className={
532
- isNoOutputPreview && (isEditable || codeShown)
739
+ parkedShowsSource
533
740
  ? "mo-slide-content flex flex-col gap-3"
534
741
  : "mo-slide-content"
535
742
  }
@@ -539,7 +746,7 @@ const RevealSlidesComponent = ({
539
746
  cell={parkedPreviewCell}
540
747
  isNoOutputPreview={isNoOutputPreview}
541
748
  isEditable={isEditable}
542
- codeShown={codeShown}
749
+ showCode={parkedShowCode}
543
750
  />
544
751
  </div>
545
752
  </div>
@@ -547,17 +754,35 @@ const RevealSlidesComponent = ({
547
754
  )}
548
755
  <div className="absolute top-2 right-2 z-20 opacity-0 group-hover:opacity-70 text-muted-foreground transition-opacity">
549
756
  {codeToggleEnabled && (
550
- <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
+ >
551
766
  <Button
552
767
  data-testid="marimo-plugin-slides-toggle-code"
553
768
  variant="ghost"
554
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.
555
772
  className={cn(
556
773
  "text-muted-foreground h-7 w-7",
557
- codeShown && "text-foreground bg-muted",
774
+ cellShowsCode && "text-foreground bg-muted",
775
+ codeAlwaysShown && "opacity-50 cursor-not-allowed",
558
776
  )}
559
- aria-pressed={codeShown}
560
- 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
+ }
561
786
  onClick={toggleShowCode}
562
787
  >
563
788
  <CodeIcon className="h-4 w-4" />
@@ -607,6 +832,7 @@ const RevealSlidesComponent = ({
607
832
  <PanelResizeHandle
608
833
  className="mo-slides-notes-resize"
609
834
  hitAreaMargins={{ coarse: 12, fine: 4 }}
835
+ disabled={isFullscreen}
610
836
  />
611
837
  <Panel
612
838
  defaultSize={10}
@@ -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
  };