@marimo-team/islands 0.25.3-dev6 → 0.25.3-dev61

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 (236) hide show
  1. package/dist/{ConnectedDataExplorerComponent-CNMliVzw.js → ConnectedDataExplorerComponent-Da5OoEUL.js} +14 -14
  2. package/dist/{ErrorBoundary-Ca-Sa492.js → ErrorBoundary-BEfUX0zz.js} +8 -8
  3. package/dist/{abnfDiagram-VCTEODGH-D-Ez91gV.js → abnfDiagram-VCTEODGH-Bz3mrprX.js} +2 -2
  4. package/dist/any-language-editor-k2B8YI3Y.js +75 -0
  5. package/dist/{architectureDiagram-5GKGNRK7-CTpcNtO8.js → architectureDiagram-5GKGNRK7-Cs5ujb9K.js} +1 -1
  6. package/dist/{arrays-CN1djC4k.js → arrays-R6JsImiT.js} +8 -0
  7. package/dist/{button-CvjkwdSk.js → button-BxB3RDjN.js} +1 -1
  8. package/dist/{capabilities-CQ5IjhzL.js → capabilities-BgR0osfN.js} +2 -2
  9. package/dist/{chat-ui-0lKdOoQ3.js → chat-ui-C0_lCzql.js} +1139 -1140
  10. package/dist/{check-EDwSwP89.js → check-P35dc2dH.js} +1 -1
  11. package/dist/{chunk-IMKFNOWR-DGxi-zeo.js → chunk-IMKFNOWR-CcTSaxZt.js} +1 -1
  12. package/dist/{chunk-SHT3W25Y-CV9J3rAB.js → chunk-SHT3W25Y-DcJcjOaO.js} +1 -1
  13. package/dist/{chunk-SVP7TREG-BL-9Fqgg.js → chunk-SVP7TREG-BVtDTjaF.js} +1 -1
  14. package/dist/{chunk-TICWLB2K-Cu_XMjDM.js → chunk-TICWLB2K-BkDFkh8q.js} +1 -1
  15. package/dist/{chunk-YOKDWASO-DPZk8vhS.js → chunk-YOKDWASO-DKplTrBY.js} +20 -20
  16. package/dist/{classDiagram-ZZMXUADV-BXXV13Hq.js → classDiagram-ZZMXUADV-CfAt7HT5.js} +2 -2
  17. package/dist/{classDiagram-v2-VYDZK3BY-B7FXJ1Cp.js → classDiagram-v2-VYDZK3BY-CGEjNYa2.js} +2 -2
  18. package/dist/{code-visibility-NGRjhbtA.js → code-visibility-CA0UOEgk.js} +6 -6
  19. package/dist/{common-B7_ZSg0P.js → common-BgoPSdwN.js} +4960 -4835
  20. package/dist/{copy-icon-Ft7t9bqz.js → copy-icon-CPoxUG7G.js} +3 -3
  21. package/dist/{cynefinDiagram-5FMLGOSQ-CH96rwba.js → cynefinDiagram-5FMLGOSQ-BW6NmfJi.js} +1 -1
  22. package/dist/{dagre-GXQ25YYZ-BD3PmpFY.js → dagre-GXQ25YYZ-CWg3CTuF.js} +1 -1
  23. package/dist/{data-grid-overlay-editor-B_cmrkBm.js → data-grid-overlay-editor-CRjm80Ab.js} +5 -5
  24. package/dist/{data-grid-types-BH_r7g7l.js → data-grid-types-0PJ-9N5n.js} +1 -1
  25. package/dist/{diagram-S7CK7UJ4-BsSJJldB.js → diagram-S7CK7UJ4-BPxtoS-z.js} +1 -1
  26. package/dist/{diagram-UQ7AKVKN-muQtNZc4.js → diagram-UQ7AKVKN-DA-vJdFL.js} +1 -1
  27. package/dist/{diagram-VX7I27RA-CdhuZAqR.js → diagram-VX7I27RA-D6bkR84S.js} +1 -1
  28. package/dist/{diagram-Z3DM3KII-BD8mXjOj.js → diagram-Z3DM3KII-m60g4cAT.js} +1 -1
  29. package/dist/{dialog-CEBFZNyc.js → dialog-C1PcpCqX.js} +3 -3
  30. package/dist/{dist-DpBpA_n1.js → dist-Lm-D-Vr6.js} +3 -3
  31. package/dist/{dropdown-menu-CP_qCYHC.js → dropdown-menu-B3eUiVJB.js} +177 -169
  32. package/dist/{ebnfDiagram-PWID7BFC-BIN1sweE.js → ebnfDiagram-PWID7BFC-CK99sho2.js} +2 -2
  33. package/dist/{erDiagram-RLTQ6QDP-Ctp5cfoN.js → erDiagram-RLTQ6QDP-Bryq0tdE.js} +1 -1
  34. package/dist/{error-banner-D-WCIiMt.js → error-banner-Sl2FlSJ4.js} +2 -2
  35. package/dist/{error-entries-DSvFWT_M.js → error-entries-CMNtA8NK.js} +959 -959
  36. package/dist/{feedback-button-CTF2Nj9s.js → feedback-button-CZA7i0Ir.js} +29 -29
  37. package/dist/{flowDiagram-HODETNUW-3EfLrlFb.js → flowDiagram-HODETNUW-D-3aRpfS.js} +2 -2
  38. package/dist/{formats-CeOmU1MU.js → formats-6pLWWrY2.js} +3 -3
  39. package/dist/{ganttDiagram-EL5Y4UJY-BQT16JSc.js → ganttDiagram-EL5Y4UJY-Dh_4cBJZ.js} +2 -2
  40. package/dist/{glide-data-editor-BQHF9puC.js → glide-data-editor-Bd52MLtf.js} +370 -368
  41. package/dist/{glide-utils-DJyqh0EM.js → glide-utils-Bt8_pBa2.js} +16 -16
  42. package/dist/{globals-Cj8iqMEp.js → globals-BvANmUu6.js} +3 -3
  43. package/dist/{highlighted-body-KPVGNVTW-zwjq21tf.js → highlighted-body-KPVGNVTW-3Ziwfz-V.js} +2 -2
  44. package/dist/{html-to-image-B8s5SgGm.js → html-to-image-6xcoTKhC.js} +29 -29
  45. package/dist/{infoDiagram-27XIBGKW-DipQoHxl.js → infoDiagram-27XIBGKW-s1C1kgfU.js} +1 -1
  46. package/dist/{ishikawaDiagram-5VMMS53U-DgFMjSGz.js → ishikawaDiagram-5VMMS53U-CCsRgolY.js} +1 -1
  47. package/dist/{kanban-definition-UXKFOSKX-w1ACVjgl.js → kanban-definition-UXKFOSKX-BrmxDlcc.js} +1 -1
  48. package/dist/{label-BDKF3Afc.js → label-BRnUC0NX.js} +2 -2
  49. package/dist/{linear-Cnaq3Tfb.js → linear-DWERh7eI.js} +121 -73
  50. package/dist/{loader-BQOfjTIJ.js → loader-Bm8cy3e8.js} +12 -12
  51. package/dist/main.js +3177 -2980
  52. package/dist/{mermaid-CHo49Yv-.js → mermaid-DKI0869I.js} +38 -38
  53. package/dist/mermaid-HWGCJPDP-Du3-HPQA.js +2 -0
  54. package/dist/{mindmap-definition-YA3MSWOX-CrT0KSfX.js → mindmap-definition-YA3MSWOX-641xJysF.js} +1 -1
  55. package/dist/{number-overlay-editor-DBcnm0SY.js → number-overlay-editor-C7ffluCS.js} +3 -3
  56. package/dist/{objects-CzUgoMIF.js → objects-DCmMvZtn.js} +1 -3
  57. package/dist/{once-BtE-wKRL.js → once-eG8Vmvjd.js} +1 -1
  58. package/dist/{pegDiagram-XKGWAZYB-BqvywNJA.js → pegDiagram-XKGWAZYB-keVUAUeS.js} +2 -2
  59. package/dist/{pieDiagram-E7YTZNPT-BfxJ_XCc.js → pieDiagram-E7YTZNPT-DHktXPO_.js} +1 -1
  60. package/dist/precisionRound-DhAzeRcm.js +49 -0
  61. package/dist/{process-output-CKRXyszb.js → process-output-BJRJFobf.js} +4 -4
  62. package/dist/{quadrantDiagram-AXDQQJYC-Ccw0xMjN.js → quadrantDiagram-AXDQQJYC-Blrm7V7_.js} +1 -1
  63. package/dist/{railroadDiagram-O6MQD6OU-8vFLVTPu.js → railroadDiagram-O6MQD6OU-BVEF4OV_.js} +2 -2
  64. package/dist/{react-vega-BED0DjiB.js → react-vega-C4T9Rb_w.js} +12284 -12270
  65. package/dist/{requirementDiagram-BXWQKSXE-a_MxGvxe.js → requirementDiagram-BXWQKSXE-CWo-UAKV.js} +1 -1
  66. package/dist/{reveal-component-DTBSuGkC.js → reveal-component-Dfuz8zQ_.js} +19 -19
  67. package/dist/{select-BButFi-d.js → select-B0LRDH0g.js} +4 -4
  68. package/dist/{sequenceDiagram-WJ2MYXX4-CXFMnxGh.js → sequenceDiagram-WJ2MYXX4-CHQvLnq4.js} +1 -1
  69. package/dist/{spec-BwarBpUJ.js → spec-C-LRKGtT.js} +4 -4
  70. package/dist/{stateDiagram-D77RDMKH-DQeA_CwI.js → stateDiagram-D77RDMKH-BOJZgbUJ.js} +2 -2
  71. package/dist/{stateDiagram-v2-MP3YSRHH-BxxQucm1.js → stateDiagram-v2-MP3YSRHH-C53XXdYH.js} +2 -2
  72. package/dist/style.css +1 -1
  73. package/dist/{swimlanes-42K2YHIH-Do6L-cYg.js → swimlanes-42K2YHIH-D2o1gOtw.js} +1 -1
  74. package/dist/{swimlanesDiagram-VR7AAH4N-DUzPvSVU.js → swimlanesDiagram-VR7AAH4N-BHFzN1Wk.js} +2 -2
  75. package/dist/{swiper-component-DKntyn8p.js → swiper-component-DfhRvAFi.js} +2 -2
  76. package/dist/{time-BieKwdYS.js → time-BaMbJs4z.js} +18 -18
  77. package/dist/{timeline-definition-24CTP7MA-D3c46WM5.js → timeline-definition-24CTP7MA-rv7QUa46.js} +1 -1
  78. package/dist/{toDate-vIXljnsr.js → toDate-CjPuEPou.js} +21 -20
  79. package/dist/{toast-B9UMEi8u.js → toast-CFWpihJU.js} +4 -4
  80. package/dist/{tooltip-BO1REbqs.js → tooltip-CKO1pHJo.js} +2 -2
  81. package/dist/{use-restore-focus-BEM7sFWl.js → use-restore-focus-DBCITLeF.js} +1 -1
  82. package/dist/{useAsyncData-Deh5kszT.js → useAsyncData-ChgNtdmE.js} +15 -14
  83. package/dist/{useDateFormatter-D8XruNGG.js → useDateFormatter-c_yJtkAe.js} +2 -2
  84. package/dist/{useDeepCompareMemoize-DXwzRINI.js → useDeepCompareMemoize-Bet7kSsI.js} +1 -1
  85. package/dist/{useIframeCapabilities-DBETvvPl.js → useIframeCapabilities-BkdeVdXC.js} +1 -1
  86. package/dist/{useLifecycle-j7D9uD1o.js → useLifecycle-VVmz_24o.js} +3 -3
  87. package/dist/{useTheme-CRnjkku0.js → useTheme-DympCsQg.js} +5 -5
  88. package/dist/{vega-component-DTG6zxaY.js → vega-component-DnfQeoyt.js} +147 -147
  89. package/dist/{vega-embed-container-BNHE18G9.js → vega-embed-container--LYZqguO.js} +2 -2
  90. package/dist/{vega-loader.browser-DNST6uVf.js → vega-loader.browser-B9Pz0EGP.js} +298 -507
  91. package/dist/{vennDiagram-4TSXK5OY-BSZD8JcZ.js → vennDiagram-4TSXK5OY-D36Zk7t-.js} +1 -1
  92. package/dist/{wardleyDiagram-VM6X3IG4-48cUWkfA.js → wardleyDiagram-VM6X3IG4-BLBolvyQ.js} +1 -1
  93. package/dist/{xychartDiagram-S5SC5T6Z-CswYgYeu.js → xychartDiagram-S5SC5T6Z-CfH57l0v.js} +2 -2
  94. package/package.json +4 -4
  95. package/src/__tests__/host-imports.test.ts +1 -0
  96. package/src/components/app-config/ai-config.tsx +6 -2
  97. package/src/components/chat/__tests__/message-queue.test.tsx +17 -0
  98. package/src/components/chat/chat-panel.tsx +3 -7
  99. package/src/components/chat/chat-utils.ts +4 -8
  100. package/src/components/data-table/__tests__/context-menu-interaction.test.tsx +130 -0
  101. package/src/components/data-table/__tests__/date-popover.test.tsx +72 -0
  102. package/src/components/data-table/__tests__/export-actions.test.tsx +434 -6
  103. package/src/components/data-table/charts/charts.tsx +14 -19
  104. package/src/components/data-table/column-visibility-dropdown.tsx +8 -13
  105. package/src/components/data-table/columns.tsx +2 -1
  106. package/src/components/data-table/context-menu.tsx +36 -20
  107. package/src/components/data-table/date-popover.tsx +6 -4
  108. package/src/components/data-table/export-actions.tsx +267 -163
  109. package/src/components/data-table/row-viewer-panel/row-viewer.tsx +2 -2
  110. package/src/components/data-table/schemas.ts +14 -36
  111. package/src/components/editor/__tests__/dynamic-favicon.test.tsx +111 -18
  112. package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +24 -0
  113. package/src/components/editor/actions/pair-with-agent-modal.tsx +18 -15
  114. package/src/components/editor/ai/add-cell-with-ai.tsx +3 -3
  115. package/src/components/editor/cell/CreateCellButton.tsx +8 -10
  116. package/src/components/editor/cell/__tests__/useShouldShowInterrupt.test.ts +39 -0
  117. package/src/components/editor/cell/code/__tests__/language-toggle.test.tsx +54 -0
  118. package/src/components/editor/cell/code/cell-editor.tsx +1 -1
  119. package/src/components/editor/cell/code/language-toggle.tsx +8 -7
  120. package/src/components/editor/cell/useShouldShowInterrupt.ts +2 -12
  121. package/src/components/editor/chrome/panels/__tests__/packages-panel.test.tsx +30 -1
  122. package/src/components/editor/chrome/panels/packages-panel.tsx +4 -7
  123. package/src/components/editor/chrome/wrapper/__tests__/attachment-status.test.tsx +12 -18
  124. package/src/components/editor/connections/components.tsx +1 -1
  125. package/src/components/editor/controls/__tests__/command-palette.test.tsx +53 -0
  126. package/src/components/editor/controls/command-palette.tsx +22 -10
  127. package/src/components/editor/dynamic-favicon.tsx +21 -40
  128. package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +12 -23
  129. package/src/components/editor/file-tree/__tests__/file-explorer-interactions.test.tsx +6 -0
  130. package/src/components/editor/file-tree/__tests__/use-file-search.test.tsx +91 -0
  131. package/src/components/editor/file-tree/file-explorer.tsx +10 -5
  132. package/src/components/editor/file-tree/file-viewer.tsx +4 -2
  133. package/src/components/editor/file-tree/use-file-search.ts +35 -21
  134. package/src/components/editor/output/MarimoTracebackOutput.tsx +1 -1
  135. package/src/components/editor/output/console/ConsoleOutput.tsx +8 -18
  136. package/src/components/icons/__tests__/large-spinner.test.tsx +39 -0
  137. package/src/components/icons/large-spinner.tsx +3 -14
  138. package/src/components/pages/home-page.tsx +4 -4
  139. package/src/components/slides/slide-cell-view.tsx +1 -1
  140. package/src/components/tracing/tracing.tsx +2 -2
  141. package/src/core/attachments/state.ts +5 -8
  142. package/src/core/cells/__tests__/notebook-outline.test.ts +113 -0
  143. package/src/core/cells/cells.ts +2 -1
  144. package/src/core/cells/pending-delete-service.ts +2 -6
  145. package/src/core/datasets/__tests__/data-source-discovery.test.ts +4 -4
  146. package/src/core/datasets/data-source-discovery.ts +4 -6
  147. package/src/core/edit-app.tsx +2 -2
  148. package/src/core/host/AGENTS.md +11 -2
  149. package/src/core/host/__tests__/attachments.test.ts +56 -0
  150. package/src/core/host/__tests__/document.test.ts +126 -0
  151. package/src/core/host/__tests__/events.test.ts +49 -0
  152. package/src/core/host/__tests__/fixtures.ts +41 -0
  153. package/src/core/host/__tests__/notebook-client.test.ts +496 -0
  154. package/src/core/host/connection.ts +81 -0
  155. package/src/core/host/events.ts +68 -0
  156. package/src/core/host/notebook-client.ts +316 -0
  157. package/src/core/host/resources/attachments.ts +19 -0
  158. package/src/core/host/resources/document.ts +101 -0
  159. package/src/core/hotkeys/actions.ts +2 -4
  160. package/src/core/islands/__tests__/bootstrap.test.ts +0 -26
  161. package/src/core/islands/bootstrap.ts +0 -1
  162. package/src/core/network/connection.ts +4 -2
  163. package/src/core/network/useConnectionNotice.ts +8 -2
  164. package/src/core/run-app.tsx +2 -2
  165. package/src/core/runtime/__tests__/runtime.test.ts +5 -12
  166. package/src/core/runtime/runtime.ts +5 -5
  167. package/src/core/static/files.ts +2 -0
  168. package/src/core/websocket/__tests__/{useMarimoKernelConnection.test.ts → connection-state.test.ts} +15 -17
  169. package/src/core/websocket/__tests__/{useWebSocket.test.ts → createConnectionTransport.test.ts} +1 -1
  170. package/src/core/websocket/__tests__/useNotebookClient.hook.test.tsx +149 -0
  171. package/src/core/websocket/__tests__/{useMarimoKernelConnection.hook.test.tsx → useNotebookSession.hook.test.tsx} +168 -253
  172. package/src/core/websocket/connection-state.ts +70 -0
  173. package/src/core/websocket/state.ts +7 -0
  174. package/src/core/websocket/transports/__tests__/sse.test.ts +18 -4
  175. package/src/core/websocket/transports/create.ts +31 -0
  176. package/src/core/websocket/transports/sse.ts +14 -2
  177. package/src/core/websocket/transports/transport.ts +12 -23
  178. package/src/core/websocket/types.ts +9 -39
  179. package/src/core/websocket/useNotebookClient.ts +78 -0
  180. package/src/core/websocket/{useMarimoKernelConnection.tsx → useNotebookSession.tsx} +53 -233
  181. package/src/hooks/__tests__/useAsyncData.test.tsx +148 -0
  182. package/src/hooks/__tests__/useDebounce.test.tsx +51 -0
  183. package/src/hooks/__tests__/useDeepCompareMemoize.test.tsx +39 -0
  184. package/src/hooks/__tests__/useDelayElapsed.test.tsx +28 -0
  185. package/src/hooks/__tests__/useInternalStateWithSync.test.tsx +20 -0
  186. package/src/hooks/__tests__/useScript.test.tsx +77 -0
  187. package/src/hooks/__tests__/useStateForKey.test.tsx +257 -0
  188. package/src/hooks/useAsyncData.ts +3 -3
  189. package/src/hooks/useDataSourceDiscovery.ts +6 -4
  190. package/src/hooks/useDebounce.ts +5 -8
  191. package/src/hooks/useDeepCompareMemoize.ts +1 -1
  192. package/src/hooks/useDelayElapsed.ts +14 -10
  193. package/src/hooks/useElapsedTime.ts +3 -3
  194. package/src/hooks/useInputHistory.ts +4 -7
  195. package/src/hooks/useInternalStateWithSync.ts +6 -6
  196. package/src/hooks/useResizeObserver.ts +4 -7
  197. package/src/hooks/useScript.ts +67 -70
  198. package/src/hooks/useStateForKey.ts +93 -0
  199. package/src/plugins/core/registerReactComponent.tsx +1 -1
  200. package/src/plugins/impl/DataTablePlugin.tsx +14 -15
  201. package/src/plugins/impl/DropdownPlugin.tsx +2 -6
  202. package/src/plugins/impl/FileBrowserPlugin.tsx +4 -16
  203. package/src/plugins/impl/FormPlugin.tsx +1 -1
  204. package/src/plugins/impl/SliderPlugin.tsx +6 -8
  205. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +201 -1
  206. package/src/plugins/impl/__tests__/FileBrowserPlugin.test.tsx +45 -0
  207. package/src/plugins/impl/__tests__/SliderPlugin.test.tsx +24 -3
  208. package/src/plugins/impl/anywidget/__tests__/registry.test.ts +2 -0
  209. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +241 -2
  210. package/src/plugins/impl/anywidget/runtime.ts +21 -9
  211. package/src/plugins/impl/anywidget/widget-binding.ts +160 -82
  212. package/src/plugins/impl/chat/__tests__/chat-prompts.test.tsx +93 -0
  213. package/src/plugins/impl/chat/chat-ui.tsx +10 -9
  214. package/src/plugins/impl/code/any-language-editor.tsx +2 -2
  215. package/src/plugins/impl/data-editor/glide-data-editor.tsx +8 -5
  216. package/src/plugins/impl/data-editor/glide-utils.ts +3 -1
  217. package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +5 -3
  218. package/src/plugins/impl/panel/PanelPlugin.tsx +15 -16
  219. package/src/plugins/impl/panel/__tests__/utils.test.ts +39 -1
  220. package/src/plugins/impl/panel/utils.ts +17 -0
  221. package/src/plugins/impl/vega/make-selectable.ts +3 -6
  222. package/src/stories/__tests__/editor-story.test.tsx +33 -0
  223. package/src/stories/editor.stories.tsx +24 -17
  224. package/src/utils/__tests__/arrays.test.ts +35 -0
  225. package/src/utils/__tests__/numbers.test.ts +48 -1
  226. package/src/utils/__tests__/objects.test.ts +21 -0
  227. package/src/utils/arrays.ts +10 -0
  228. package/src/utils/numbers.ts +28 -17
  229. package/src/utils/objects.ts +3 -7
  230. package/src/utils/once.ts +19 -19
  231. package/dist/any-language-editor-DZ_YTVus.js +0 -76
  232. package/dist/mermaid-HWGCJPDP-ByCsZnYF.js +0 -2
  233. package/dist/precisionRound-DMIVQrt2.js +0 -97
  234. package/src/core/host/__tests__/protocol.test.ts +0 -214
  235. package/src/core/host/protocol.ts +0 -1943
  236. package/src/core/websocket/useWebSocket.tsx +0 -88
@@ -1,5 +1,5 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
- import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
3
  import { EventBuffer, extractBuffers, MessageSchema } from "../utils";
4
4
 
5
5
  describe("MessageSchema", () => {
@@ -73,6 +73,44 @@ describe("EventBuffer", () => {
73
73
  eventBuffer = new EventBuffer(processEvents, 50);
74
74
  });
75
75
 
76
+ afterEach(() => {
77
+ eventBuffer.dispose();
78
+ vi.useRealTimers();
79
+ });
80
+
81
+ it("flushes queued events and cancels the pending timer on dispose", () => {
82
+ eventBuffer.add("event1");
83
+ eventBuffer.add("event2");
84
+ expect(vi.getTimerCount()).toBe(1);
85
+ expect(processEvents).toHaveBeenCalledOnce();
86
+
87
+ eventBuffer.dispose();
88
+ expect(processEvents).toHaveBeenCalledTimes(2);
89
+ eventBuffer.dispose();
90
+
91
+ expect(eventBuffer.size()).toBe(0);
92
+ expect(vi.getTimerCount()).toBe(0);
93
+ vi.advanceTimersByTime(100);
94
+ expect(processEvents).toHaveBeenCalledTimes(2);
95
+ });
96
+
97
+ it("does not process an empty buffer on dispose", () => {
98
+ eventBuffer.dispose();
99
+ expect(processEvents).not.toHaveBeenCalled();
100
+ });
101
+
102
+ it("resets the blocked state when disposed", () => {
103
+ eventBuffer.add("event1");
104
+ // Emulate a real processEvents, which drains the buffer
105
+ eventBuffer.getAndClear();
106
+ eventBuffer.dispose();
107
+ eventBuffer.add("event2");
108
+
109
+ expect(processEvents).toHaveBeenCalledTimes(2);
110
+ expect(eventBuffer.getAndClear()).toEqual(["event2"]);
111
+ expect(vi.getTimerCount()).toBe(1);
112
+ });
113
+
76
114
  it("should add events to buffer", () => {
77
115
  eventBuffer.add("event1");
78
116
  eventBuffer.add("event2");
@@ -83,6 +83,7 @@ export class EventBuffer<T> {
83
83
  clearTimeout(this.timeout);
84
84
  }
85
85
  this.timeout = window.setTimeout(() => {
86
+ this.timeout = null;
86
87
  this.isBlocked = false;
87
88
  if (this.buffer.length > 0) {
88
89
  this.flush();
@@ -98,6 +99,22 @@ export class EventBuffer<T> {
98
99
  this.buffer = [];
99
100
  }
100
101
 
102
+ /**
103
+ * Cancel the pending timer, flushing any events still held by the block
104
+ * window so the last user interaction is not lost.
105
+ */
106
+ public dispose() {
107
+ if (this.timeout !== null) {
108
+ clearTimeout(this.timeout);
109
+ this.timeout = null;
110
+ }
111
+ this.isBlocked = false;
112
+ if (this.buffer.length > 0) {
113
+ this.processEvents();
114
+ }
115
+ this.clear();
116
+ }
117
+
101
118
  public getAndClear() {
102
119
  const events = this.buffer;
103
120
  this.buffer = [];
@@ -3,6 +3,7 @@
3
3
  import type { TopLevelSpec } from "vega-lite";
4
4
  import type { NonNormalizedSpec } from "vega-lite/types_unstable/spec/index.js";
5
5
  import type { TopLevelParameter } from "vega-lite/types_unstable/spec/toplevel.js";
6
+ import { Arrays } from "@/utils/arrays";
6
7
 
7
8
  import { findEncodedFields, makeEncodingInteractive } from "./encodings";
8
9
  import { Marks } from "./marks";
@@ -357,9 +358,7 @@ export function makeSelectable<T extends VegaLiteSpec>(
357
358
 
358
359
  // If fieldSelection is an array, filter the fields
359
360
  if (Array.isArray(fieldSelection)) {
360
- legendFields = legendFields.filter((field) =>
361
- fieldSelection.includes(field),
362
- );
361
+ legendFields = Arrays.intersection(legendFields, fieldSelection);
363
362
  }
364
363
  }
365
364
 
@@ -420,9 +419,7 @@ function makeLegendSelectable(
420
419
  let legendFields = findEncodedFields(spec);
421
420
  // If fieldSelection is an array, we filter the fields
422
421
  if (Array.isArray(fieldSelection)) {
423
- legendFields = legendFields.filter((field) =>
424
- fieldSelection.includes(field),
425
- );
422
+ legendFields = Arrays.intersection(legendFields, fieldSelection);
426
423
  }
427
424
 
428
425
  const legendParams = legendFields.map((field) => Params.legend(field));
@@ -0,0 +1,33 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { EditorView } from "@codemirror/view";
3
+ import { composeStories } from "@storybook/react-vite";
4
+ import { act, render } from "@testing-library/react";
5
+ import { expect, it, vi } from "vitest";
6
+ import * as stories from "../editor.stories";
7
+
8
+ vi.mock("@/core/codemirror/cm", () => ({
9
+ basicBundle: () => [],
10
+ }));
11
+
12
+ const { Primary } = composeStories(stories);
13
+
14
+ it("preserves editor changes when the story rerenders with the same theme", () => {
15
+ const { container, rerender } = render(<Primary />);
16
+ const editor = container.querySelector<HTMLElement>(".cm-editor");
17
+ expect(editor).not.toBeNull();
18
+ const view = editor && EditorView.findFromDOM(editor);
19
+ expect(view).toBeDefined();
20
+ if (!view) {
21
+ throw new Error("Editor view was not mounted");
22
+ }
23
+
24
+ act(() => {
25
+ view.dispatch({
26
+ changes: { from: 0, to: view.state.doc.length, insert: "user edit" },
27
+ });
28
+ });
29
+ rerender(<Primary />);
30
+
31
+ expect(container.querySelector(".cm-editor")).toBe(editor);
32
+ expect(view.state.doc.toString()).toBe("user edit");
33
+ });
@@ -5,7 +5,7 @@ import { EditorState, type Extension } from "@codemirror/state";
5
5
  import { EditorView } from "@codemirror/view";
6
6
  import type { Meta, StoryObj } from "@storybook/react-vite";
7
7
  import CodeMirror from "@uiw/react-codemirror";
8
- import { useEffect, useRef } from "react";
8
+ import { useEffect, useMemo, useRef } from "react";
9
9
  import { CopilotConfig } from "@/core/codemirror/copilot/copilot-config";
10
10
  import { copilotBundle } from "@/core/codemirror/copilot/extension";
11
11
  import { OverridingHotkeyProvider } from "@/core/hotkeys/hotkeys";
@@ -52,28 +52,35 @@ const Editor = (opts: { extensions?: Extension[] }): React.ReactNode => {
52
52
  });
53
53
 
54
54
  return () => view.destroy();
55
- // oxlint-disable-next-line react-hooks/exhaustive-deps
56
- }, [ref.current]);
55
+ }, [opts.extensions]);
57
56
 
58
57
  return <div className="cm" ref={ref} />;
59
58
  };
60
59
 
61
- export const Primary: Story = {
62
- render: (_args, ctx) => (
60
+ const PrimaryEditor = ({ theme }: Pick<CodeMirrorSetupOpts, "theme">) => {
61
+ const extensions = useMemo(
62
+ () =>
63
+ basicBundle({
64
+ completionConfig: {
65
+ activate_on_typing: false,
66
+ copilot: false,
67
+ signature_hint_on_typing: false,
68
+ },
69
+ theme,
70
+ hotkeys: new OverridingHotkeyProvider({}),
71
+ } as unknown as CodeMirrorSetupOpts),
72
+ [theme],
73
+ );
74
+
75
+ return (
63
76
  <div className="Cell m-20 w-[60%] overflow-hidden">
64
- <Editor
65
- extensions={basicBundle({
66
- completionConfig: {
67
- activate_on_typing: false,
68
- copilot: false,
69
- signature_hint_on_typing: false,
70
- },
71
- theme: ctx.globals.theme,
72
- hotkeys: new OverridingHotkeyProvider({}),
73
- } as unknown as CodeMirrorSetupOpts)}
74
- />
77
+ <Editor extensions={extensions} />
75
78
  </div>
76
- ),
79
+ );
80
+ };
81
+
82
+ export const Primary: Story = {
83
+ render: (_args, ctx) => <PrimaryEditor theme={ctx.globals.theme} />,
77
84
  };
78
85
 
79
86
  export const DefaultPython: Story = {
@@ -16,6 +16,41 @@ import {
16
16
  } from "../arrays";
17
17
 
18
18
  describe("arrays", () => {
19
+ describe.each([
20
+ ["intersection", Arrays.intersection, [3, 3, 2], [Number.NaN]],
21
+ ["difference", Arrays.difference, [1], [1]],
22
+ ] as const)("%s", (_name, operation, expected, expectedWithNaN) => {
23
+ it("preserves order and duplicates without mutating readonly inputs", () => {
24
+ const values = Object.freeze([3, 1, 3, 2]);
25
+ const other = Object.freeze([2, 3, 3]);
26
+ expect(operation(values, other)).toEqual(expected);
27
+ expect(values).toEqual([3, 1, 3, 2]);
28
+ expect(other).toEqual([2, 3, 3]);
29
+ });
30
+
31
+ it("uses the same equality as array membership, including NaN", () => {
32
+ expect(operation([Number.NaN, 1], [Number.NaN])).toEqual(expectedWithNaN);
33
+ });
34
+
35
+ it("handles an empty source", () => {
36
+ expect(operation([], [1])).toEqual([]);
37
+ });
38
+ });
39
+
40
+ it("handles empty membership lists", () => {
41
+ expect(Arrays.intersection([1, 1, 2], [])).toEqual([]);
42
+ expect(Arrays.difference([1, 1, 2], [])).toEqual([1, 1, 2]);
43
+ });
44
+
45
+ it("compares objects by identity", () => {
46
+ const shared = { id: 1 };
47
+ const distinct = { id: 1 };
48
+ expect(Arrays.intersection([shared, distinct], [shared])).toEqual([shared]);
49
+ expect(Arrays.difference([shared, distinct], [shared])).toEqual([distinct]);
50
+ expect(Arrays.intersection([shared, distinct], [shared])[0]).toBe(shared);
51
+ expect(Arrays.difference([shared, distinct], [shared])[0]).toBe(distinct);
52
+ });
53
+
19
54
  describe("arrayDelete", () => {
20
55
  it("should delete an element at the specified index", () => {
21
56
  expect(arrayDelete([1, 2, 3], 1)).toEqual([1, 3]);
@@ -1,5 +1,5 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
- import { describe, expect, it } from "vitest";
2
+ import { describe, expect, it, vi } from "vitest";
3
3
  import {
4
4
  countFractionDigits,
5
5
  maxFractionDigitsForSteps,
@@ -20,6 +20,53 @@ describe("prettyNumber", () => {
20
20
 
21
21
  const options = { locale };
22
22
 
23
+ describe("formatter reuse", () => {
24
+ it.each([
25
+ {
26
+ name: "scientific",
27
+ value: 0.000_123_4,
28
+ format: (value: number, locale: string) =>
29
+ prettyScientificNumber(value, { locale }),
30
+ },
31
+ {
32
+ name: "rounded",
33
+ value: 1234.567,
34
+ format: (value: number, locale: string) =>
35
+ prettyScientificNumber(value, { locale, shouldRound: true }),
36
+ },
37
+ {
38
+ name: "engineering",
39
+ value: 12_345,
40
+ format: prettyEngineeringNumber,
41
+ },
42
+ ])(
43
+ "reuses the $name formatter until the locale changes",
44
+ ({ format, value }) => {
45
+ const NumberFormat = Intl.NumberFormat;
46
+ const constructor = vi
47
+ .spyOn(Intl, "NumberFormat")
48
+ .mockImplementation(function (locales, options) {
49
+ return new NumberFormat(locales, options);
50
+ });
51
+ try {
52
+ const first = format(value, "fr-FR");
53
+ expect(format(value, "fr-FR")).toBe(first);
54
+ format(value * 2, "fr-FR");
55
+ expect(constructor).toHaveBeenCalledTimes(1);
56
+
57
+ format(value, "de-DE");
58
+ expect(constructor).toHaveBeenCalledTimes(2);
59
+ expect(constructor).toHaveBeenLastCalledWith(
60
+ "de-DE",
61
+ expect.any(Object),
62
+ );
63
+ } finally {
64
+ constructor.mockRestore();
65
+ }
66
+ },
67
+ );
68
+ });
69
+
23
70
  describe("prettyScientificNumber", () => {
24
71
  it("should handle special cases", () => {
25
72
  expect(prettyScientificNumber(0, options)).toBe("0");
@@ -204,6 +204,27 @@ describe("Objects", () => {
204
204
  });
205
205
 
206
206
  describe("filter", () => {
207
+ it.each([42, { base_url: "https://example.com" }])(
208
+ "preserves an own __proto__ property with value %j",
209
+ (value) => {
210
+ const obj = { ["__proto__"]: value, removed: value };
211
+ const result = Objects.filter(obj, (_, key) => key !== "removed");
212
+
213
+ expect(Object.entries(result)).toEqual([["__proto__", value]]);
214
+ expect(Object.getPrototypeOf(result)).toBe(Object.prototype);
215
+ expect(Object.getOwnPropertyDescriptor(result, "__proto__")).toEqual({
216
+ value,
217
+ writable: true,
218
+ enumerable: true,
219
+ configurable: true,
220
+ });
221
+ expect(Object.keys(obj)).toEqual(["__proto__", "removed"]);
222
+ expect(Objects.filter(obj, (_, key) => key !== "__proto__")).toEqual({
223
+ removed: value,
224
+ });
225
+ },
226
+ );
227
+
207
228
  it("should filter object entries by predicate", () => {
208
229
  const obj = { a: 1, b: 2, c: 3, d: 4 };
209
230
  const result = Objects.filter(obj, (v) => v % 2 === 0);
@@ -55,6 +55,16 @@ export function arrayShallowEquals<T>(a: T[], b: T[]): boolean {
55
55
  export const Arrays = {
56
56
  // oxlint-disable-next-line typescript/no-explicit-any
57
57
  EMPTY: [] as any,
58
+ /** Keep values present in both arrays, preserving the first array's order and duplicates. */
59
+ intersection<T>(values: readonly T[], included: readonly T[]): T[] {
60
+ const includedSet = new Set(included);
61
+ return values.filter((value) => includedSet.has(value));
62
+ },
63
+ /** Remove values present in the second array, preserving order and duplicates. */
64
+ difference<T>(values: readonly T[], excluded: readonly T[]): T[] {
65
+ const excludedSet = new Set(excluded);
66
+ return values.filter((value) => !excludedSet.has(value));
67
+ },
58
68
  zip: <T, U>(a: T[], b: U[]): [T, U][] => {
59
69
  invariant(a.length === b.length, "Arrays must be the same length");
60
70
  return a.map((item, i) => [item, b[i]]);
@@ -23,6 +23,31 @@ export const maxFractionalDigits = memoizeLastValue((locale: string) => {
23
23
  return 0;
24
24
  });
25
25
 
26
+ const getScientificFormatter = memoizeLastValue(
27
+ (locale: string) =>
28
+ new Intl.NumberFormat(locale, {
29
+ minimumFractionDigits: 1,
30
+ maximumFractionDigits: 1,
31
+ notation: "scientific",
32
+ }),
33
+ );
34
+
35
+ const getRoundedFormatter = memoizeLastValue(
36
+ (locale: string) =>
37
+ new Intl.NumberFormat(locale, {
38
+ minimumFractionDigits: 0,
39
+ maximumFractionDigits: 2,
40
+ }),
41
+ );
42
+
43
+ const getEngineeringFormatter = memoizeLastValue(
44
+ (locale: string) =>
45
+ new Intl.NumberFormat(locale, {
46
+ notation: "engineering",
47
+ maximumSignificantDigits: 3,
48
+ }),
49
+ );
50
+
26
51
  /** Decimal places needed to display `n` without float noise. */
27
52
  export function countFractionDigits(n: number): number {
28
53
  if (!Number.isFinite(n) || n === 0) {
@@ -108,23 +133,14 @@ export function prettyScientificNumber(
108
133
  // Determine if the number should be in scientific notation
109
134
  const absValue = Math.abs(value);
110
135
  if (absValue < 1e-2 || absValue >= 1e6) {
111
- return new Intl.NumberFormat(opts.locale, {
112
- minimumFractionDigits: 1,
113
- maximumFractionDigits: 1,
114
- notation: "scientific",
115
- })
116
- .format(value)
117
- .toLowerCase();
136
+ return getScientificFormatter(opts.locale).format(value).toLowerCase();
118
137
  }
119
138
 
120
139
  const { shouldRound, locale } = opts;
121
140
 
122
141
  if (shouldRound) {
123
142
  // Number has an integer part, format with 2 decimal places
124
- return new Intl.NumberFormat(locale, {
125
- minimumFractionDigits: 0,
126
- maximumFractionDigits: 2,
127
- }).format(value);
143
+ return getRoundedFormatter(locale).format(value);
128
144
  }
129
145
 
130
146
  // Don't round
@@ -161,12 +177,7 @@ export function prettyEngineeringNumber(value: number, locale: string): string {
161
177
  return specialCase;
162
178
  }
163
179
 
164
- const [mant, exp] = new Intl.NumberFormat(locale, {
165
- notation: "engineering",
166
- maximumSignificantDigits: 3,
167
- })
168
- .format(value)
169
- .split("E");
180
+ const [mant, exp] = getEngineeringFormatter(locale).format(value).split("E");
170
181
 
171
182
  if (exp in prefixes) {
172
183
  return mant + prefixes[exp as keyof typeof prefixes];
@@ -89,13 +89,9 @@ export const Objects = {
89
89
  obj: Record<K, V>,
90
90
  predicate: (value: V, key: K) => boolean,
91
91
  ): Record<K, V> {
92
- const result: Record<K, V> = {} as Record<K, V>;
93
- for (const [key, value] of Objects.entries(obj)) {
94
- if (predicate(value, key)) {
95
- result[key] = obj[key];
96
- }
97
- }
98
- return result;
92
+ return Objects.fromEntries(
93
+ Objects.entries(obj).filter(([key, value]) => predicate(value, key)),
94
+ );
99
95
  },
100
96
 
101
97
  omit<V extends object, K extends keyof V>(
package/src/utils/once.ts CHANGED
@@ -2,36 +2,36 @@
2
2
 
3
3
  import { arrayShallowEquals } from "./arrays";
4
4
 
5
- // oxlint-disable-next-line typescript/no-explicit-any
6
- export function once<T extends (...args: any[]) => any>(fn: T): T {
7
- let result: ReturnType<T>;
5
+ export function once<F extends (...args: never[]) => unknown>(fn: F): F;
6
+ export function once<Args extends unknown[], Result, This>(
7
+ fn: (this: This, ...args: Args) => Result,
8
+ ): (this: This, ...args: Args) => Result {
9
+ let result: Result;
8
10
  let called = false;
9
- return function (
10
- this: ThisParameterType<T>,
11
- ...args: Parameters<T>
12
- ): ReturnType<T> {
11
+ return function (this: This, ...args: Args): Result {
13
12
  if (!called) {
14
13
  called = true;
15
- result = fn.apply(this, args) as ReturnType<T>;
14
+ result = fn.apply(this, args);
16
15
  }
17
16
  return result;
18
- } as T;
17
+ };
19
18
  }
20
19
 
21
- // oxlint-disable-next-line typescript/no-explicit-any
22
- export function memoizeLastValue<T extends (...args: any[]) => any>(fn: T): T {
23
- let result: ReturnType<T>;
24
- let lastArgs: Parameters<T> | undefined;
20
+ export function memoizeLastValue<F extends (...args: never[]) => unknown>(
21
+ fn: F,
22
+ ): F;
23
+ export function memoizeLastValue<Args extends unknown[], Result, This>(
24
+ fn: (this: This, ...args: Args) => Result,
25
+ ): (this: This, ...args: Args) => Result {
26
+ let result: Result;
27
+ let lastArgs: Args | undefined;
25
28
  let lastError: unknown;
26
29
  let hasError = false;
27
30
 
28
- return function (
29
- this: ThisParameterType<T>,
30
- ...args: Parameters<T>
31
- ): ReturnType<T> {
31
+ return function (this: This, ...args: Args): Result {
32
32
  if (lastArgs === undefined || !arrayShallowEquals(args, lastArgs)) {
33
33
  try {
34
- result = fn.apply(this, args) as ReturnType<T>;
34
+ result = fn.apply(this, args);
35
35
  hasError = false;
36
36
  lastError = undefined;
37
37
  } catch (error) {
@@ -45,5 +45,5 @@ export function memoizeLastValue<T extends (...args: any[]) => any>(fn: T): T {
45
45
  throw lastError;
46
46
  }
47
47
  return result;
48
- } as T;
48
+ };
49
49
  }
@@ -1,76 +0,0 @@
1
- import "./rolldown-runtime-Cb76FRpK.js";
2
- import { g as e } from "./button-CvjkwdSk.js";
3
- import { t } from "./react-sHtZdjeP.js";
4
- import { t as n } from "./compiler-runtime-CldRdSsv.js";
5
- import { t as r } from "./copy-icon-Ft7t9bqz.js";
6
- import { t as i } from "./jsx-runtime-3BD1kT0s.js";
7
- import { n as a } from "./error-banner-D-WCIiMt.js";
8
- import { t as o } from "./esm-AMFssoNp.js";
9
- import { n as s, t as c } from "./esm-D2gTsXEV.js";
10
- //#region src/plugins/impl/code/any-language-editor.tsx
11
- var l = n();
12
- t();
13
- var u = i(), d = {
14
- python: "py",
15
- python3: "py",
16
- javascript: "js",
17
- typescript: "ts",
18
- shell: "sh",
19
- bash: "sh",
20
- unknown: "text",
21
- undefined: "text"
22
- }, f = [];
23
- function p(e) {
24
- return e ? e in c : !1;
25
- }
26
- var m = (t) => {
27
- let n = (0, l.c)(29), i, m, h, g, _, v;
28
- if (n[0] !== t) {
29
- let { language: e, hideUnsupportedLanguageErrors: r, showCopyButton: a, extensions: o, ...s } = t;
30
- m = r, _ = a, g = s, h = e, i = o === void 0 ? f : o, h = d[h || ""] || h, v = p(h), n[0] = t, n[1] = i, n[2] = m, n[3] = h, n[4] = g, n[5] = _, n[6] = v;
31
- } else i = n[1], m = n[2], h = n[3], g = n[4], _ = n[5], v = n[6];
32
- let y = v;
33
- y || e.warn(`Language ${h} not found in CodeMirror.`);
34
- let b;
35
- bb0: {
36
- if (!p(h)) {
37
- b = i;
38
- break bb0;
39
- }
40
- let e;
41
- n[7] === h ? e = n[8] : (e = s(h), n[7] = h, n[8] = e);
42
- let t;
43
- n[9] !== i || n[10] !== e ? (t = [e, ...i].filter(Boolean), n[9] = i, n[10] = e, n[11] = t) : t = n[11], b = t;
44
- }
45
- let x = b, S;
46
- n[12] !== m || n[13] !== y || n[14] !== h ? (S = !y && !m && /* @__PURE__ */ (0, u.jsx)(a, {
47
- className: "mb-1 rounded-sm",
48
- error: `Language ${h} not supported. Supported languages are: ${Object.keys(c).join(", ")}`
49
- }), n[12] = m, n[13] = y, n[14] = h, n[15] = S) : S = n[15];
50
- let C;
51
- n[16] !== y || n[17] !== g || n[18] !== _ ? (C = _ && y && /* @__PURE__ */ (0, u.jsx)(r, {
52
- tooltip: !1,
53
- buttonClassName: "absolute top-2 right-2 z-10 hover-action",
54
- className: "h-4 w-4 text-muted-foreground",
55
- value: g.value || "",
56
- toastTitle: "Copied to clipboard"
57
- }), n[16] = y, n[17] = g, n[18] = _, n[19] = C) : C = n[19];
58
- let w;
59
- n[20] === x ? w = n[21] : (w = [...x], n[20] = x, n[21] = w);
60
- let T;
61
- n[22] !== g || n[23] !== w ? (T = /* @__PURE__ */ (0, u.jsx)(o, {
62
- ...g,
63
- extensions: w
64
- }), n[22] = g, n[23] = w, n[24] = T) : T = n[24];
65
- let E;
66
- return n[25] !== S || n[26] !== C || n[27] !== T ? (E = /* @__PURE__ */ (0, u.jsxs)("div", {
67
- className: "relative w-full group hover-actions-parent",
68
- children: [
69
- S,
70
- C,
71
- T
72
- ]
73
- }), n[25] = S, n[26] = C, n[27] = T, n[28] = E) : E = n[28], E;
74
- };
75
- //#endregion
76
- export { d as LANGUAGE_MAP, m as default };
@@ -1,2 +0,0 @@
1
- import { i as e } from "./chunk-YOKDWASO-DPZk8vhS.js";
2
- export { e as Mermaid };