@marimo-team/frontend 0.23.16-dev3 → 0.23.16-dev31

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 (226) hide show
  1. package/dist/assets/{CellStatus-BalYv0pK.js → CellStatus-CmojVYzm.js} +1 -1
  2. package/dist/assets/{ConnectedDataExplorerComponent-DI7mV-Gw.js → ConnectedDataExplorerComponent-CMaZapRd.js} +1 -1
  3. package/dist/assets/{ErrorBoundary-C7_5jjWi.js → ErrorBoundary-DzTD8LVq.js} +1 -1
  4. package/dist/assets/{ImperativeModal-BIavOiUv.js → ImperativeModal-fY-ZfT0b.js} +1 -1
  5. package/dist/assets/{JsonOutput-CkCD-FgM.js → JsonOutput-BwwRpByA.js} +12 -12
  6. package/dist/assets/{LazyAnyLanguageCodeMirror-sVm_hz8A.js → LazyAnyLanguageCodeMirror-B2JvSXP-.js} +2 -2
  7. package/dist/assets/{MarimoErrorOutput-DO2k2fA5.js → MarimoErrorOutput-BWte8vjk.js} +4 -4
  8. package/dist/assets/{RSPContexts-CgC81f0I.js → RSPContexts-WiCgGm__.js} +1 -1
  9. package/dist/assets/RenderHTML-DRltV71H.js +1 -0
  10. package/dist/assets/{RunButton-BKRupqr7.js → RunButton-DIegd2mN.js} +1 -1
  11. package/dist/assets/{add-cell-with-ai-CEZtaZCK.js → add-cell-with-ai-BBFD-M9V.js} +17 -17
  12. package/dist/assets/add-connection-dialog-CECJ8507.js +170 -0
  13. package/dist/assets/{agent-panel-CHB6t7Zy.js → agent-panel-YaS0WXP1.js} +5 -5
  14. package/dist/assets/ai-model-dropdown-BpVBUZnz.js +5 -0
  15. package/dist/assets/{alert-dialog-BXegNC0I.js → alert-dialog-Bb8Lz8ko.js} +1 -1
  16. package/dist/assets/{any-language-editor-DCpZRrpx.js → any-language-editor--tRHs8C_.js} +1 -1
  17. package/dist/assets/{app-config-button-NrM9ywXA.js → app-config-button-BW-w01aF.js} +1 -1
  18. package/dist/assets/{button-eIihcbUw.js → button-DDGiD1NW.js} +1 -1
  19. package/dist/assets/{cache-panel-wllSTlbM.js → cache-panel-DcQUeUpg.js} +1 -1
  20. package/dist/assets/{capabilities-B09j1Eor.js → capabilities-DoEg0XwL.js} +1 -1
  21. package/dist/assets/{cell-editor-B5JVq7_q.js → cell-editor-EV-1IccQ.js} +10 -10
  22. package/dist/assets/cell-link-C5gpAn3R.js +1 -0
  23. package/dist/assets/cells-6yd8nOAu.js +231 -0
  24. package/dist/assets/{chat-display-BG08ltQ3.js → chat-display-8BbC5B-D.js} +1 -1
  25. package/dist/assets/{chat-panel-P2CzCtXJ.js → chat-panel-00V3uFDx.js} +3 -3
  26. package/dist/assets/{chat-ui-fAJZAoTD.js → chat-ui-DWhJ2Ozy.js} +1 -1
  27. package/dist/assets/{column-preview-Dne1dIt0.js → column-preview-DeXESYpM.js} +1 -1
  28. package/dist/assets/{command-CAJLPEBq.js → command-Dqxie4gq.js} +1 -1
  29. package/dist/assets/{command-palette-iOCqUIs1.js → command-palette-DZ2rNzzf.js} +1 -1
  30. package/dist/assets/{common-CD6Mj8n6.js → common-XVELED_O.js} +1 -1
  31. package/dist/assets/components-BLHpmhXx.js +1 -0
  32. package/dist/assets/config-B-Q7J_6d.js +1 -0
  33. package/dist/assets/{config-COUOIPEq.js → config-BJVx3eBW.js} +1 -1
  34. package/dist/assets/{copy-BtTOqPTS.js → copy-Bxb1_Gtw.js} +1 -1
  35. package/dist/assets/{copy-icon-C2pG0ZNK.js → copy-icon-neDQUo4Q.js} +1 -1
  36. package/dist/assets/{createReducer-C7kZ8zEe.js → createReducer-DGx5Af-C.js} +1 -1
  37. package/dist/assets/{datasource-ySAiC9gr.js → datasource-CTqa8bf-.js} +2 -2
  38. package/dist/assets/{dates-D-kIKDYA.js → dates-C2LWzsCZ.js} +1 -1
  39. package/dist/assets/{dependency-graph-panel-BF23EwcD.js → dependency-graph-panel-C4Nzcddp.js} +4 -4
  40. package/dist/assets/{dialog-D1DEsRZ2.js → dialog-BCVepWLJ.js} +1 -1
  41. package/dist/assets/{dist-ByVeMWkr.js → dist-BLN8PNJV.js} +1 -1
  42. package/dist/assets/dist-CiVShZKM.js +2 -0
  43. package/dist/assets/{documentation-panel-e9Fi9R7e.js → documentation-panel-Bym4G7IS.js} +1 -1
  44. package/dist/assets/download-HKgqJX4l.js +9 -0
  45. package/dist/assets/{dropdown-menu-BFZLnWeO.js → dropdown-menu-dHmZR8bI.js} +1 -1
  46. package/dist/assets/{edit-page-DSEkwJ4O.js → edit-page-D80ascX2.js} +6 -6
  47. package/dist/assets/{error-banner-B6eIlxzg.js → error-banner-BSxEOIn2.js} +1 -1
  48. package/dist/assets/{error-entries-Birw-8VX.js → error-entries-CxyvaxEM.js} +2 -2
  49. package/dist/assets/{error-panel-DdsUL6b7.js → error-panel-CfSy7uUr.js} +1 -1
  50. package/dist/assets/{es-1D83cgiu.js → es-gYgfF8y5.js} +1 -1
  51. package/dist/assets/{esm-BL7jQmzn.js → esm-B30N1GS0.js} +1 -1
  52. package/dist/assets/{field-BeZbVzXA.js → field-BsxD35VE.js} +1 -1
  53. package/dist/assets/file-explorer-panel-Dd5Xn2TK.js +26 -0
  54. package/dist/assets/{file-icons-Cb54mIL8.js → file-icons-CSjJ3JC6.js} +1 -1
  55. package/dist/assets/{file-name-input-BWFcK1ey.js → file-name-input-CrKtrXMl.js} +1 -1
  56. package/dist/assets/floating-outline-ByQ8vvif.js +1 -0
  57. package/dist/assets/focus-CVONl-Dy.js +1 -0
  58. package/dist/assets/{form-B6BkSCJb.js → form-Ca3yd_Od.js} +1 -1
  59. package/dist/assets/{formats-ClEFGPy-.js → formats-DzmLpc9A.js} +1 -1
  60. package/dist/assets/{formatting-B_41gSF5.js → formatting-CDf6w1Kt.js} +1 -1
  61. package/dist/assets/{fullscreen-dUjPYOmm.js → fullscreen-CTDqNQr6.js} +1 -1
  62. package/dist/assets/{gallery-page-CWQQAYpa.js → gallery-page-CJ3ms1Hn.js} +1 -1
  63. package/dist/assets/{glide-data-editor-lMARFWHS.js → glide-data-editor-j2igs9Y_.js} +1 -1
  64. package/dist/assets/home-page-FpAawj9s.js +4 -0
  65. package/dist/assets/{html-to-image-CwmX28T9.js → html-to-image-C-4ysbGe.js} +2 -2
  66. package/dist/assets/index-4k5eCUzO.js +38 -0
  67. package/dist/assets/index-xs-zSSrX.css +2 -0
  68. package/dist/assets/{input-CqrUjynb.js → input-JxFwu-3G.js} +1 -1
  69. package/dist/assets/kiosk-mode-BumOFEtY.js +1 -0
  70. package/dist/assets/{label-BElwWzf0.js → label-CqjrpwRC.js} +1 -1
  71. package/dist/assets/{layout-CUlZijwG.js → layout-Dtql_o_T.js} +5 -5
  72. package/dist/assets/{links-CToBjIIj.js → links-CZr5bjvo.js} +1 -1
  73. package/dist/assets/{logs-panel-iFV4ZJ9b.js → logs-panel-5u1wUBn1.js} +1 -1
  74. package/dist/assets/{markdown-renderer-QvXG0CRk.js → markdown-renderer-dABDqZDw.js} +2 -2
  75. package/dist/assets/{menu-items-DbSLBfXR.js → menu-items-CCi6aOPm.js} +1 -1
  76. package/dist/assets/{mermaid-Bh0flEIb.js → mermaid-CYN0wOyG.js} +1 -1
  77. package/dist/assets/name-cell-input-Y4knmEIU.js +1 -0
  78. package/dist/assets/node-sql-parser-Si4oZ2P-.js +68 -0
  79. package/dist/assets/{numbers-CeeaFKqu.js → numbers-rmawRqQP.js} +1 -1
  80. package/dist/assets/outline-panel-CQ_KWLip.js +1 -0
  81. package/dist/assets/{packages-panel-BzQF60rG.js → packages-panel-Ijt7d51P.js} +1 -1
  82. package/dist/assets/{pair-with-agent-modal-CKOYwOYw.js → pair-with-agent-modal-BUtnUuiE.js} +1 -1
  83. package/dist/assets/panels-DfU1Oikj.js +4 -0
  84. package/dist/assets/{popover-DrHQ3KEt.js → popover-Bcz1Zikb.js} +1 -1
  85. package/dist/assets/process-output-Dd11YI04.js +1 -0
  86. package/dist/assets/{radio-group-CLBjGkt5.js → radio-group-22eWJ9fv.js} +1 -1
  87. package/dist/assets/readonly-code-display-BjK-TEri.js +1 -0
  88. package/dist/assets/{readonly-python-code-CW_G5e4E.js → readonly-python-code-n_TrLmsR.js} +1 -1
  89. package/dist/assets/{renderShortcut-BZLr8D7H.js → renderShortcut-Bl8_FVn8.js} +1 -1
  90. package/dist/assets/{reveal-component-BLMTmd8T.js → reveal-component-CCLTV1q6.js} +60 -60
  91. package/dist/assets/{run-page-B6pgDYEJ.js → run-page-DbezK7Qs.js} +1 -1
  92. package/dist/assets/{runs-D51Xwrsk.js → runs-DBTXqJHS.js} +1 -1
  93. package/dist/assets/scratchpad-panel-C2wAEv5n.js +1 -0
  94. package/dist/assets/{scroll-to-bottom-button-Cgr10kaJ.js → scroll-to-bottom-button-C8yfT4wq.js} +1 -1
  95. package/dist/assets/{secrets-panel-Cc5KDBk6.js → secrets-panel-6J6wcuVR.js} +1 -1
  96. package/dist/assets/{select-B5TFNjmU.js → select-BP75_5jJ.js} +1 -1
  97. package/dist/assets/{session-CEFnN9Cq.js → session-BmukRjSi.js} +1 -1
  98. package/dist/assets/{session-panel-Y0Oon0sD.js → session-panel-BZGNcZmp.js} +1 -1
  99. package/dist/assets/{share-CYpcX4N4.js → share-852X_cqy.js} +1 -1
  100. package/dist/assets/{skeleton-D3QPxqWC.js → skeleton-BzlKcq21.js} +1 -1
  101. package/dist/assets/slides-DkEgzTJB.css +1 -0
  102. package/dist/assets/{snippets-panel-CXpGflMO.js → snippets-panel-DNaNtkR0.js} +1 -1
  103. package/dist/assets/{spec-CzqcmYQk.js → spec-C7opZZRp.js} +1 -1
  104. package/dist/assets/{state-BvdK8eiw.js → state-D10U2MwA.js} +1 -1
  105. package/dist/assets/{state-WBgc7MiA.js → state-DDag0B_B.js} +1 -1
  106. package/dist/assets/state-DFPAfLw7.js +3 -0
  107. package/dist/assets/{state-DBzEQtWz.js → state-jCdh8Dcx.js} +1 -1
  108. package/dist/assets/{strings-Djnbwmfl.js → strings--BknHX9C.js} +1 -1
  109. package/dist/assets/{swiper-component-DInvCxhI.js → swiper-component-g8EXE911.js} +1 -1
  110. package/dist/assets/switch-OVajJBPR.js +1 -0
  111. package/dist/assets/{terminal-D3IQBBYo.js → terminal-217O8hlZ.js} +1 -1
  112. package/dist/assets/{textarea-CpFfEUet.js → textarea-DuEtSOCl.js} +1 -1
  113. package/dist/assets/{tooltip-CFX4fYHU.js → tooltip-4bZlU7jl.js} +1 -1
  114. package/dist/assets/{tooltip-HLb6RxVp.js → tooltip-B7U4xkIN.js} +1 -1
  115. package/dist/assets/{tracing-panel-BU-o_LCd.js → tracing-panel-rVAa3En0.js} +2 -2
  116. package/dist/assets/{tracing-CKn3gWHw.js → tracing-ttUd3J7U.js} +1 -1
  117. package/dist/assets/{tree-actions-CsC2XmGo.js → tree-actions-UCr1tMHw.js} +1 -1
  118. package/dist/assets/{useBoolean-D89u30n-.js → useBoolean-CFj86iac.js} +1 -1
  119. package/dist/assets/useCellActionButton-CJknR0XY.js +1 -0
  120. package/dist/assets/{useDeleteCell-EPH4kcG7.js → useDeleteCell-PrgejW8A.js} +1 -1
  121. package/dist/assets/{useDependencyPanelTab-CZnK7N2L.js → useDependencyPanelTab-5jc-_-hy.js} +1 -1
  122. package/dist/assets/useEventListener-C0195JsF.js +1 -0
  123. package/dist/assets/{useHotkey-DIuRH8Wu.js → useHotkey-DLZvrRZT.js} +1 -1
  124. package/dist/assets/{useIframeCapabilities-Ccb4fvKc.js → useIframeCapabilities-j6fR_dBA.js} +1 -1
  125. package/dist/assets/{useInstallPackage-BRbUAUFn.js → useInstallPackage-WiYBwhUO.js} +1 -1
  126. package/dist/assets/{useInterval-CC0vKpWm.js → useInterval-CWBTUy5W.js} +1 -1
  127. package/dist/assets/useNotebookActions-DG8f4m8p.js +12 -0
  128. package/dist/assets/useRunCells-DHsd7HGL.js +1 -0
  129. package/dist/assets/{useSplitCell-9Qz93yPZ.js → useSplitCell-MZpER0Bh.js} +1 -1
  130. package/dist/assets/{useTheme-KXqAyDPC.js → useTheme-Bxv9B31g.js} +1 -1
  131. package/dist/assets/{utils-Z80SV4_d.js → utils-BWBBpCkT.js} +1 -1
  132. package/dist/assets/{vega-component-Cie3ag_T.js → vega-component-Db8KjZJG.js} +1 -1
  133. package/dist/assets/{write-secret-modal-DEEn7sZV.js → write-secret-modal-3-v-dHVj.js} +1 -1
  134. package/dist/index.html +68 -68
  135. package/package.json +2 -2
  136. package/src/__mocks__/requests.ts +45 -4
  137. package/src/components/data-table/__tests__/__snapshots__/chart-spec-model.test.ts.snap +174 -21
  138. package/src/components/data-table/__tests__/chart-spec-model.test.ts +102 -1
  139. package/src/components/data-table/column-summary/chart-spec-model.tsx +163 -103
  140. package/src/components/data-table/column-summary/legacy-chart-spec.ts +15 -7
  141. package/src/components/data-table/column-summary/utils.ts +26 -7
  142. package/src/components/data-table/types.ts +1 -1
  143. package/src/components/editor/actions/__tests__/pdf-export.test.ts +0 -4
  144. package/src/components/editor/actions/pdf-export.ts +1 -4
  145. package/src/components/editor/actions/useNotebookActions.tsx +7 -13
  146. package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +50 -0
  147. package/src/components/editor/connections/storage/__tests__/as-code.test.ts +87 -8
  148. package/src/components/editor/connections/storage/as-code.ts +40 -21
  149. package/src/components/editor/connections/storage/schemas.ts +83 -42
  150. package/src/components/editor/file-tree/__tests__/file-render-mode.test.ts +27 -0
  151. package/src/components/editor/file-tree/__tests__/file-viewer.test.tsx +150 -0
  152. package/src/components/editor/file-tree/file-preview-metadata.tsx +51 -0
  153. package/src/components/editor/file-tree/file-render-mode.ts +32 -0
  154. package/src/components/editor/file-tree/file-viewer.tsx +76 -50
  155. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +1 -1
  156. package/src/components/slides/__tests__/cell-output-block.test.tsx +40 -0
  157. package/src/components/slides/__tests__/slide-cell-view.test.tsx +33 -0
  158. package/src/components/slides/__tests__/slide-scroll-hint.test.tsx +198 -0
  159. package/src/components/slides/reveal-component.tsx +25 -19
  160. package/src/components/slides/slide-cell-view.tsx +1 -1
  161. package/src/components/slides/slide-scroll-hint.tsx +161 -0
  162. package/src/components/slides/slides.css +4 -2
  163. package/src/components/static-html/share-modal.tsx +1 -1
  164. package/src/components/ui/input.tsx +1 -1
  165. package/src/core/codemirror/find-replace/__tests__/navigate.test.ts +42 -30
  166. package/src/core/codemirror/find-replace/navigate.ts +2 -0
  167. package/src/core/codemirror/go-to-definition/__tests__/commands.test.ts +24 -3
  168. package/src/core/codemirror/go-to-definition/commands.ts +3 -0
  169. package/src/core/codemirror/keymaps/__tests__/keymaps.test.ts +31 -2
  170. package/src/core/codemirror/keymaps/keymaps.ts +12 -0
  171. package/src/core/codemirror/language/__tests__/sql.test.ts +443 -61
  172. package/src/core/codemirror/language/languages/sql/completion-store.ts +5 -1
  173. package/src/core/codemirror/language/languages/sql/sql.ts +210 -3
  174. package/src/core/codemirror/utils.ts +28 -0
  175. package/src/core/hotkeys/hotkeys.ts +0 -2
  176. package/src/core/islands/__tests__/bridge.test.ts +107 -0
  177. package/src/core/islands/bridge.ts +16 -0
  178. package/src/core/mode.ts +10 -0
  179. package/src/core/network/__tests__/api.test.ts +42 -0
  180. package/src/core/network/__tests__/connection.test.ts +45 -0
  181. package/src/core/network/__tests__/requests-lazy.test.ts +32 -1
  182. package/src/core/network/__tests__/requests-network.test.ts +21 -0
  183. package/src/core/network/api.ts +27 -0
  184. package/src/core/network/connection.ts +15 -0
  185. package/src/core/network/requests-lazy.ts +1 -0
  186. package/src/core/network/requests-network.ts +14 -8
  187. package/src/core/network/requests-static.ts +1 -0
  188. package/src/core/network/requests-toasting.tsx +1 -0
  189. package/src/core/network/types.ts +25 -7
  190. package/src/core/static/download-html.ts +5 -14
  191. package/src/core/wasm/bridge.ts +4 -2
  192. package/src/core/wasm/worker/types.ts +5 -2
  193. package/src/plugins/impl/FormPlugin.tsx +1 -1
  194. package/src/plugins/impl/__tests__/FormPlugin.test.tsx +162 -0
  195. package/src/utils/__tests__/download.test.tsx +24 -1
  196. package/src/utils/download.ts +11 -6
  197. package/dist/assets/RenderHTML-BCvU9dYz.js +0 -1
  198. package/dist/assets/add-connection-dialog-B-jCRNTN.js +0 -170
  199. package/dist/assets/ai-model-dropdown-DXgLphFa.js +0 -5
  200. package/dist/assets/cell-link-DqBkyAFe.js +0 -1
  201. package/dist/assets/cells-Cf7bHHq3.js +0 -229
  202. package/dist/assets/components-BH6gDVv6.js +0 -1
  203. package/dist/assets/config-BbOTTCYY.js +0 -1
  204. package/dist/assets/dist-BCMBu0rO.js +0 -2
  205. package/dist/assets/download-BomYt3nl.js +0 -9
  206. package/dist/assets/file-explorer-panel-BfXM2BOd.js +0 -26
  207. package/dist/assets/floating-outline-wz-WSRgP.js +0 -1
  208. package/dist/assets/focus-DiEyfiAG.js +0 -1
  209. package/dist/assets/home-page-DJ4jJsZj.js +0 -4
  210. package/dist/assets/index-A71LAt6h.css +0 -2
  211. package/dist/assets/index-Dbc5mg7a.js +0 -38
  212. package/dist/assets/kiosk-mode-ABm3oYgq.js +0 -1
  213. package/dist/assets/name-cell-input-C6cukQ9k.js +0 -1
  214. package/dist/assets/node-sql-parser-BWgUjcfV.js +0 -68
  215. package/dist/assets/outline-panel-BPlg9_CE.js +0 -1
  216. package/dist/assets/panels-Dj45fW-v.js +0 -4
  217. package/dist/assets/process-output-BEhIGfkO.js +0 -1
  218. package/dist/assets/readonly-code-display-D63oRtBP.js +0 -1
  219. package/dist/assets/scratchpad-panel-B0mEqBNk.js +0 -1
  220. package/dist/assets/slides-MCbMDtlY.css +0 -1
  221. package/dist/assets/state-BZQc7xxL.js +0 -3
  222. package/dist/assets/switch-CvweaCXq.js +0 -1
  223. package/dist/assets/useCellActionButton-CRfIEVB5.js +0 -1
  224. package/dist/assets/useEventListener-B_w3A283.js +0 -1
  225. package/dist/assets/useNotebookActions-DTXAoukG.js +0 -12
  226. package/dist/assets/useRunCells-CSSF_mJ0.js +0 -1
@@ -0,0 +1,198 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { act, render, screen } from "@testing-library/react";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+ import {
6
+ shouldShowScrollHint,
7
+ SlideScrollContainer,
8
+ } from "../slide-scroll-hint";
9
+
10
+ describe("shouldShowScrollHint", () => {
11
+ it("shows when content overflows and scroll is at the top", () => {
12
+ expect(
13
+ shouldShowScrollHint({
14
+ scrollHeight: 800,
15
+ clientHeight: 400,
16
+ scrollTop: 0,
17
+ }),
18
+ ).toBe(true);
19
+ });
20
+
21
+ it("hides when content fits", () => {
22
+ expect(
23
+ shouldShowScrollHint({
24
+ scrollHeight: 300,
25
+ clientHeight: 400,
26
+ scrollTop: 0,
27
+ }),
28
+ ).toBe(false);
29
+ });
30
+
31
+ it("hides after the user scrolls down", () => {
32
+ expect(
33
+ shouldShowScrollHint({
34
+ scrollHeight: 800,
35
+ clientHeight: 400,
36
+ scrollTop: 40,
37
+ }),
38
+ ).toBe(false);
39
+ });
40
+
41
+ it("ignores sub-pixel overflow", () => {
42
+ expect(
43
+ shouldShowScrollHint({
44
+ scrollHeight: 401,
45
+ clientHeight: 400,
46
+ scrollTop: 0,
47
+ }),
48
+ ).toBe(false);
49
+ });
50
+
51
+ it("still shows within the top threshold", () => {
52
+ expect(
53
+ shouldShowScrollHint({
54
+ scrollHeight: 800,
55
+ clientHeight: 400,
56
+ scrollTop: 8,
57
+ }),
58
+ ).toBe(true);
59
+ });
60
+ });
61
+
62
+ describe("SlideScrollContainer", () => {
63
+ const resizeObservers: Array<{
64
+ callback: ResizeObserverCallback;
65
+ observe: ReturnType<typeof vi.fn>;
66
+ disconnect: ReturnType<typeof vi.fn>;
67
+ }> = [];
68
+
69
+ beforeEach(() => {
70
+ resizeObservers.length = 0;
71
+ vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) => {
72
+ cb(0);
73
+ return 0;
74
+ });
75
+ vi.stubGlobal("cancelAnimationFrame", vi.fn());
76
+ vi.stubGlobal(
77
+ "ResizeObserver",
78
+ class MockResizeObserver {
79
+ callback: ResizeObserverCallback;
80
+ observe = vi.fn();
81
+ unobserve = vi.fn();
82
+ disconnect = vi.fn();
83
+
84
+ constructor(callback: ResizeObserverCallback) {
85
+ this.callback = callback;
86
+ resizeObservers.push(this);
87
+ }
88
+ },
89
+ );
90
+ vi.stubGlobal(
91
+ "MutationObserver",
92
+ class MockMutationObserver {
93
+ observe = vi.fn();
94
+ disconnect = vi.fn();
95
+ takeRecords = vi.fn(() => []);
96
+ },
97
+ );
98
+ });
99
+
100
+ afterEach(() => {
101
+ vi.unstubAllGlobals();
102
+ });
103
+
104
+ function stubOverflow(
105
+ el: HTMLElement,
106
+ metrics: {
107
+ scrollHeight: number;
108
+ clientHeight: number;
109
+ scrollTop?: number;
110
+ },
111
+ ) {
112
+ Object.defineProperty(el, "scrollHeight", {
113
+ configurable: true,
114
+ get: () => metrics.scrollHeight,
115
+ });
116
+ Object.defineProperty(el, "clientHeight", {
117
+ configurable: true,
118
+ get: () => metrics.clientHeight,
119
+ });
120
+ Object.defineProperty(el, "scrollTop", {
121
+ configurable: true,
122
+ get: () => metrics.scrollTop ?? 0,
123
+ set: (value: number) => {
124
+ metrics.scrollTop = value;
125
+ },
126
+ });
127
+ }
128
+
129
+ it("renders a scroll hint when content overflows", () => {
130
+ render(
131
+ <SlideScrollContainer>
132
+ <div>tall content</div>
133
+ </SlideScrollContainer>,
134
+ );
135
+
136
+ const container = screen.getByTestId("slide-scroll-container");
137
+ stubOverflow(container, { scrollHeight: 900, clientHeight: 400 });
138
+
139
+ act(() => {
140
+ for (const observer of resizeObservers) {
141
+ observer.callback([], observer as unknown as ResizeObserver);
142
+ }
143
+ });
144
+
145
+ expect(screen.getByTestId("slide-scroll-hint")).toBeTruthy();
146
+ expect(screen.getByText("Scroll for more")).toBeTruthy();
147
+ });
148
+
149
+ it("hides the hint after scrolling", () => {
150
+ const metrics = {
151
+ scrollHeight: 900,
152
+ clientHeight: 400,
153
+ scrollTop: 0,
154
+ };
155
+
156
+ render(
157
+ <SlideScrollContainer>
158
+ <div>tall content</div>
159
+ </SlideScrollContainer>,
160
+ );
161
+
162
+ const container = screen.getByTestId("slide-scroll-container");
163
+ stubOverflow(container, metrics);
164
+
165
+ act(() => {
166
+ for (const observer of resizeObservers) {
167
+ observer.callback([], observer as unknown as ResizeObserver);
168
+ }
169
+ });
170
+ expect(screen.getByTestId("slide-scroll-hint")).toBeTruthy();
171
+
172
+ act(() => {
173
+ metrics.scrollTop = 50;
174
+ container.dispatchEvent(new Event("scroll"));
175
+ });
176
+
177
+ expect(screen.queryByTestId("slide-scroll-hint")).toBeNull();
178
+ });
179
+
180
+ it("does not show a hint when content fits", () => {
181
+ render(
182
+ <SlideScrollContainer>
183
+ <div>short content</div>
184
+ </SlideScrollContainer>,
185
+ );
186
+
187
+ const container = screen.getByTestId("slide-scroll-container");
188
+ stubOverflow(container, { scrollHeight: 200, clientHeight: 400 });
189
+
190
+ act(() => {
191
+ for (const observer of resizeObservers) {
192
+ observer.callback([], observer as unknown as ResizeObserver);
193
+ }
194
+ });
195
+
196
+ expect(screen.queryByTestId("slide-scroll-hint")).toBeNull();
197
+ });
198
+ });
@@ -13,7 +13,9 @@ import useEvent from "react-use-event-hook";
13
13
  import { CodeIcon, ExpandIcon, EyeOffIcon } from "lucide-react";
14
14
  import { Deck, Fragment, Slide, Stack } from "@revealjs/react";
15
15
  import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
16
+ import { cellDomProps } from "@/components/editor/common";
16
17
  import { Slide as CellOutputSlide } from "@/components/slides/slide";
18
+ import { SlideScrollContainer } from "@/components/slides/slide-scroll-hint";
17
19
  import { Button } from "@/components/ui/button";
18
20
  import { useFullScreenElement } from "@/components/ui/fullscreen";
19
21
  import { Tooltip } from "@/components/ui/tooltip";
@@ -303,6 +305,25 @@ function resolveSlideContentStyle(
303
305
  }
304
306
  }
305
307
 
308
+ /**
309
+ * Renders a cell's output wrapped in a `data-cell-id`/`data-cell-name` div
310
+ * (see `cellDomProps`), so it can be targeted by custom CSS in the slides
311
+ * view just like it can in the vertical layout. The wrapper is required
312
+ * because `CellOutputSlide` sets its own `id` from `CellOutputId` (a
313
+ * different scheme than `HTMLCellId`) and its inner `OutputArea` uses
314
+ * `display: contents`, which can't be targeted for background/border styling.
315
+ */
316
+ export const CellOutputBlock = ({ cell }: { cell: RuntimeCell }) => (
317
+ <div className="flex flex-col" {...cellDomProps(cell.id, cell.name)}>
318
+ <CellOutputSlide
319
+ cellId={cell.id}
320
+ status={cell.status}
321
+ output={cell.output}
322
+ stale={outputIsStale(cell, false)}
323
+ />
324
+ </div>
325
+ );
326
+
306
327
  const SubslideView = ({
307
328
  subslide,
308
329
  resolveShowCode,
@@ -327,7 +348,7 @@ const SubslideView = ({
327
348
 
328
349
  return (
329
350
  <Slide>
330
- <div className="h-full w-full overflow-auto flex">
351
+ <SlideScrollContainer>
331
352
  <div
332
353
  className={
333
354
  anyCodeShown
@@ -339,15 +360,7 @@ const SubslideView = ({
339
360
  {subslide.blocks.map((block, i) => {
340
361
  const rendered = block.cells.map((cell) => {
341
362
  if (!resolveShowCode(cell.id)) {
342
- return (
343
- <CellOutputSlide
344
- key={cell.id}
345
- cellId={cell.id}
346
- status={cell.status}
347
- output={cell.output}
348
- stale={outputIsStale(cell, false)}
349
- />
350
- );
363
+ return <CellOutputBlock key={cell.id} cell={cell} />;
351
364
  }
352
365
  return isEditable ? (
353
366
  <SlideCellView key={cell.id} cell={cell} />
@@ -367,7 +380,7 @@ const SubslideView = ({
367
380
  return <ReactFragment key={i}>{rendered}</ReactFragment>;
368
381
  })}
369
382
  </div>
370
- </div>
383
+ </SlideScrollContainer>
371
384
  {/* Outside any `.fragment`: shown only before any fragment is revealed. */}
372
385
  {slideLevel && <NotesAside text={slideLevel} />}
373
386
  </Slide>
@@ -405,14 +418,7 @@ const ParkedPreviewContent = ({
405
418
  <SlideCellReadOnlyView cell={cell} />
406
419
  );
407
420
  }
408
- return (
409
- <CellOutputSlide
410
- cellId={cell.id}
411
- status={cell.status}
412
- output={cell.output}
413
- stale={outputIsStale(cell, false)}
414
- />
415
- );
421
+ return <CellOutputBlock cell={cell} />;
416
422
  };
417
423
 
418
424
  // There is an upstream react bug in dev mode (https://github.com/facebook/react/issues/34840)
@@ -202,7 +202,7 @@ export const SlideCellReadOnlyView = ({ cell }: { cell: RuntimeCell }) => {
202
202
  );
203
203
 
204
204
  const editor = (
205
- <div className="marimo-cell">
205
+ <div className="marimo-cell" {...cellDomProps(cell.id, cell.name)}>
206
206
  <ReadonlyCode
207
207
  code={display.code}
208
208
  language={display.language}
@@ -0,0 +1,161 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { ChevronDownIcon } from "lucide-react";
4
+ import {
5
+ type ReactNode,
6
+ type RefObject,
7
+ useEffect,
8
+ useRef,
9
+ useState,
10
+ } from "react";
11
+ import { cn } from "@/utils/cn";
12
+
13
+ /** Pixels of scroll before the hint is considered dismissed. */
14
+ const TOP_THRESHOLD_PX = 8;
15
+ /** Ignore sub-pixel overflow from rounding / borders. */
16
+ const OVERFLOW_TOLERANCE_PX = 1;
17
+
18
+ /**
19
+ * Whether a scroll container should show a "more content below" affordance.
20
+ * Visible only when content overflows and the viewport is still near the top.
21
+ */
22
+ export function shouldShowScrollHint(options: {
23
+ scrollHeight: number;
24
+ clientHeight: number;
25
+ scrollTop: number;
26
+ }): boolean {
27
+ const { scrollHeight, clientHeight, scrollTop } = options;
28
+ const overflowing = scrollHeight > clientHeight + OVERFLOW_TOLERANCE_PX;
29
+ const atTop = scrollTop <= TOP_THRESHOLD_PX;
30
+ return overflowing && atTop;
31
+ }
32
+
33
+ /**
34
+ * Measure only up to the first unrevealed fragment so
35
+ * the hint reflects currently revealed content, not future fragment steps.
36
+ */
37
+ function measureRevealedHeight(el: HTMLElement): number {
38
+ const firstUnrevealed = el.querySelector<HTMLElement>(
39
+ ".fragment:not(.visible)",
40
+ );
41
+ if (!firstUnrevealed) {
42
+ return el.scrollHeight;
43
+ }
44
+ const fragmentTop = firstUnrevealed.getBoundingClientRect().top;
45
+ return fragmentTop - el.getBoundingClientRect().top + el.scrollTop;
46
+ }
47
+
48
+ /**
49
+ * Tracks whether a scroll container is overflowing at the top, so we can
50
+ * surface a scroll affordance.
51
+ */
52
+ function useScrollHint(
53
+ scrollRef: RefObject<HTMLElement | null>,
54
+ contentRef: RefObject<HTMLElement | null>,
55
+ ): boolean {
56
+ const [showHint, setShowHint] = useState(false);
57
+
58
+ useEffect(() => {
59
+ const el = scrollRef.current;
60
+ const content = contentRef.current;
61
+ if (!el || !content) {
62
+ return;
63
+ }
64
+
65
+ let frame = 0;
66
+ const update = () => {
67
+ cancelAnimationFrame(frame);
68
+ frame = requestAnimationFrame(() => {
69
+ setShowHint(
70
+ shouldShowScrollHint({
71
+ scrollHeight: measureRevealedHeight(el),
72
+ clientHeight: el.clientHeight,
73
+ scrollTop: el.scrollTop,
74
+ }),
75
+ );
76
+ });
77
+ };
78
+
79
+ update();
80
+
81
+ const resizeObserver = new ResizeObserver(update);
82
+ resizeObserver.observe(el);
83
+ resizeObserver.observe(content);
84
+
85
+ // Fragments reveal via class changes (opacity/visibility, no layout shift),
86
+ // which the ResizeObserver can't see, so re-check on class mutations. Only
87
+ // attach on slides that have fragments to avoid reacting to unrelated class
88
+ // churn (e.g. animating widgets) on the common non-fragment slide.
89
+ const fragmentObserver = content.querySelector(".fragment")
90
+ ? new MutationObserver(update)
91
+ : null;
92
+ fragmentObserver?.observe(content, {
93
+ subtree: true,
94
+ attributes: true,
95
+ attributeFilter: ["class"],
96
+ });
97
+
98
+ el.addEventListener("scroll", update, { passive: true });
99
+
100
+ return () => {
101
+ cancelAnimationFrame(frame);
102
+ resizeObserver.disconnect();
103
+ fragmentObserver?.disconnect();
104
+ el.removeEventListener("scroll", update);
105
+ };
106
+ }, [scrollRef, contentRef]);
107
+
108
+ return showHint;
109
+ }
110
+
111
+ const ScrollHintOverlay = ({ className }: { className?: string }) => {
112
+ return (
113
+ <div
114
+ className={cn(
115
+ "pointer-events-none absolute inset-x-0 bottom-0 z-10 flex flex-col items-center",
116
+ "animate-in fade-in-0 duration-200",
117
+ className,
118
+ )}
119
+ data-testid="slide-scroll-hint"
120
+ aria-hidden={true}
121
+ >
122
+ <div className="h-16 w-full bg-linear-to-t from-background via-background/80 to-transparent" />
123
+ <div className="absolute bottom-3 flex items-center gap-1 rounded-full border border-border/60 bg-background/90 px-2.5 py-1 text-xs text-muted-foreground shadow-sm backdrop-blur-sm">
124
+ <ChevronDownIcon className="h-3.5 w-3.5" />
125
+ <span>Scroll for more</span>
126
+ </div>
127
+ </div>
128
+ );
129
+ };
130
+
131
+ /**
132
+ * Full-height scroll container for a slide. Shows a dismissible "Scroll for
133
+ * more" cue when content overflows the frame (easy to miss in fullscreen when
134
+ * OS overlay scrollbars are hidden).
135
+ */
136
+ export const SlideScrollContainer = ({
137
+ children,
138
+ className,
139
+ }: {
140
+ children: ReactNode;
141
+ className?: string;
142
+ }) => {
143
+ const scrollRef = useRef<HTMLDivElement>(null);
144
+ const contentRef = useRef<HTMLDivElement>(null);
145
+ const showHint = useScrollHint(scrollRef, contentRef);
146
+
147
+ return (
148
+ <div className={cn("relative h-full w-full", className)}>
149
+ <div
150
+ ref={scrollRef}
151
+ className="h-full w-full overflow-auto"
152
+ data-testid="slide-scroll-container"
153
+ >
154
+ <div ref={contentRef} className="flex w-full min-h-full">
155
+ {children}
156
+ </div>
157
+ </div>
158
+ {showHint && <ScrollHintOverlay />}
159
+ </div>
160
+ );
161
+ };
@@ -15,8 +15,10 @@
15
15
  width: unset;
16
16
  }
17
17
 
18
- /* If the first output is the only child, make it flex 1 */
19
- > *:only-child > .output:only-child {
18
+ /* If the first output is the only child, make it flex 1. Cells are wrapped
19
+ in a `data-cell-id`/`data-cell-name` div (see `cellDomProps`), so `.output`
20
+ is a grandchild rather than a direct child here. */
21
+ > *:only-child:has(> .output:only-child, > * > .output:only-child) {
20
22
  flex: 1;
21
23
  }
22
24
 
@@ -40,7 +40,7 @@ export const ShareStaticNotebookModal: React.FC<{
40
40
  e.preventDefault();
41
41
 
42
42
  onClose();
43
- const html = await exportAsHTML({
43
+ const { contents: html } = await exportAsHTML({
44
44
  download: false,
45
45
  includeCode: true,
46
46
  files: VirtualFileTracker.INSTANCE.filenames(),
@@ -195,7 +195,7 @@ export const OnBlurredInput = React.forwardRef<
195
195
  onChange={(event) => setInternalValue(event.target.value)}
196
196
  onBlur={() => setValue(internalValue || "")}
197
197
  onKeyDown={(event) => {
198
- if (event.key !== "Enter") {
198
+ if (event.key !== "Enter" || event.shiftKey) {
199
199
  return;
200
200
  }
201
201
  setValue(internalValue || "");
@@ -20,6 +20,26 @@ vi.mock("@/core/cells/cells", () => ({
20
20
  getAllEditorViews: () => mockGetAllEditorViews(),
21
21
  }));
22
22
 
23
+ /**
24
+ * A view stub that only carries what navigate needs
25
+ */
26
+ function createMockView(state: EditorState): EditorView {
27
+ const cell = document.createElement("div");
28
+ cell.className = "marimo-cell";
29
+ // jsdom doesn't implement scrollIntoView.
30
+ cell.scrollIntoView = vi.fn();
31
+ const dom = document.createElement("div");
32
+ cell.append(dom);
33
+
34
+ return {
35
+ state,
36
+ dom,
37
+ // Don't actually apply changes; it's complex to mock properly and the
38
+ // assertions only care that dispatch was called.
39
+ dispatch: vi.fn(),
40
+ } as unknown as EditorView;
41
+ }
42
+
23
43
  describe("navigate", () => {
24
44
  let view1: EditorView;
25
45
  let view2: EditorView;
@@ -36,20 +56,8 @@ describe("navigate", () => {
36
56
  });
37
57
 
38
58
  // Create mock views that track dispatch calls
39
- view1 = {
40
- state: state1,
41
- dispatch: vi.fn(() => {
42
- // Mock dispatch - in real tests we just need to verify it was called
43
- // Don't actually apply changes as it's complex to mock properly
44
- }),
45
- } as unknown as EditorView;
46
-
47
- view2 = {
48
- state: state2,
49
- dispatch: vi.fn(() => {
50
- // Mock dispatch - in real tests we just need to verify it was called
51
- }),
52
- } as unknown as EditorView;
59
+ view1 = createMockView(state1);
60
+ view2 = createMockView(state2);
53
61
 
54
62
  mockViews = [view1, view2];
55
63
  mockGetAllEditorViews.mockReturnValue(mockViews);
@@ -234,6 +242,22 @@ describe("navigate", () => {
234
242
 
235
243
  expect(result).toBe(false);
236
244
  });
245
+
246
+ it("should scroll the cell owning the match into view", async () => {
247
+ const cell = view1.dom.closest(".marimo-cell");
248
+ invariant(cell, "mock view should be inside a cell");
249
+
250
+ expect(findNext()).toBeTruthy();
251
+
252
+ // The scroll is deferred a frame.
253
+ await new Promise((resolve) => requestAnimationFrame(resolve));
254
+
255
+ expect(cell.scrollIntoView).toHaveBeenCalledWith({
256
+ behavior: "instant",
257
+ block: "nearest",
258
+ inline: "nearest",
259
+ });
260
+ });
237
261
  });
238
262
 
239
263
  describe("findPrev", () => {
@@ -319,10 +343,7 @@ describe("navigate", () => {
319
343
  extensions: [EditorState.readOnly.of(true)],
320
344
  });
321
345
 
322
- const readOnlyView = {
323
- state: readOnlyState,
324
- dispatch: vi.fn(),
325
- } as unknown as EditorView;
346
+ const readOnlyView = createMockView(readOnlyState);
326
347
 
327
348
  // Replace view1 with read-only view temporarily
328
349
  mockGetAllEditorViews.mockReturnValueOnce([readOnlyView, view2]);
@@ -568,10 +589,7 @@ describe("navigate", () => {
568
589
  doc: Text.of(["hello world hello there hello"]),
569
590
  });
570
591
 
571
- const testView = {
572
- state: testDoc,
573
- dispatch: vi.fn(),
574
- } as unknown as EditorView;
592
+ const testView = createMockView(testDoc);
575
593
 
576
594
  mockGetAllEditorViews.mockReturnValue([testView]);
577
595
 
@@ -613,10 +631,7 @@ describe("navigate", () => {
613
631
  doc: Text.of([""]),
614
632
  });
615
633
 
616
- const emptyView = {
617
- state: emptyState,
618
- dispatch: vi.fn(),
619
- } as unknown as EditorView;
634
+ const emptyView = createMockView(emptyState);
620
635
 
621
636
  mockGetAllEditorViews.mockReturnValue([emptyView]);
622
637
 
@@ -629,10 +644,7 @@ describe("navigate", () => {
629
644
  doc: Text.of(["orphan content"]),
630
645
  });
631
646
 
632
- const orphanView = {
633
- state: orphanState,
634
- dispatch: vi.fn(),
635
- } as unknown as EditorView;
647
+ const orphanView = createMockView(orphanState);
636
648
 
637
649
  store.set(findReplaceAtom, {
638
650
  type: "setCurrentView",
@@ -6,6 +6,7 @@ import { EditorView } from "@codemirror/view";
6
6
  import { getAllEditorViews } from "@/core/cells/cells";
7
7
  import { replaceEditorContent } from "@/core/codemirror/replace-editor-content";
8
8
  import { store } from "@/core/state/jotai";
9
+ import { scrollOwnerCell } from "../utils";
9
10
  import { asQueryCreator, type QueryType } from "./query";
10
11
  import { findReplaceAtom } from "./state";
11
12
 
@@ -85,6 +86,7 @@ const findInDirection = (direction: "next" | "prev") =>
85
86
  effects: [EditorView.scrollIntoView(selection.main, { y: "center" })],
86
87
  userEvent: "select.search",
87
88
  });
89
+ scrollOwnerCell(view);
88
90
  store.set(findReplaceAtom, {
89
91
  type: "setCurrentView",
90
92
  view,
@@ -3,14 +3,14 @@
3
3
  import { python } from "@codemirror/lang-python";
4
4
  import { EditorState } from "@codemirror/state";
5
5
  import { EditorView } from "@codemirror/view";
6
- import { afterEach, describe, expect, test } from "vitest";
6
+ import { afterEach, describe, expect, test, vi } from "vitest";
7
7
  import { goToVariableDefinition } from "../commands";
8
8
 
9
9
  async function tick(): Promise<void> {
10
10
  await new Promise((resolve) => requestAnimationFrame(resolve));
11
11
  }
12
12
 
13
- function createEditor(content: string) {
13
+ function createEditor(content: string, parent: HTMLElement = document.body) {
14
14
  const state = EditorState.create({
15
15
  doc: content,
16
16
  extensions: [python()],
@@ -18,7 +18,7 @@ function createEditor(content: string) {
18
18
 
19
19
  const view = new EditorView({
20
20
  state,
21
- parent: document.body,
21
+ parent,
22
22
  });
23
23
 
24
24
  return view;
@@ -383,6 +383,27 @@ print('myVar')`);
383
383
  "
384
384
  `);
385
385
  });
386
+
387
+ test("scrolls the cell owning the definition into view", async () => {
388
+ const cell = document.createElement("div");
389
+ cell.className = "marimo-cell";
390
+ // jsdom doesn't implement scrollIntoView.
391
+ cell.scrollIntoView = vi.fn();
392
+ document.body.append(cell);
393
+
394
+ view = createEditor("myVar = 10\nprint(myVar)", cell);
395
+ expect(goToVariableDefinition(view, "myVar")).toBe(true);
396
+ // The jump and the scroll are both deferred to the next frame.
397
+ await tick();
398
+
399
+ expect(cell.scrollIntoView).toHaveBeenCalledWith({
400
+ behavior: "instant",
401
+ block: "nearest",
402
+ inline: "nearest",
403
+ });
404
+
405
+ cell.remove();
406
+ });
386
407
  });
387
408
 
388
409
  /**
@@ -4,6 +4,7 @@ import { syntaxTree } from "@codemirror/language";
4
4
  import type { EditorState } from "@codemirror/state";
5
5
  import { EditorView } from "@codemirror/view";
6
6
  import type { SyntaxNode, Tree, TreeCursor } from "@lezer/common";
7
+ import { scrollOwnerCell } from "../utils";
7
8
 
8
9
  const SCOPE_CREATING_NODES = new Set([
9
10
  "FunctionDefinition",
@@ -45,6 +46,8 @@ function goToPosition(view: EditorView, from: number): void {
45
46
  }),
46
47
  });
47
48
  });
49
+
50
+ scrollOwnerCell(view);
48
51
  }
49
52
 
50
53
  function findFirstMatchingVariable(