@marimo-team/islands 0.20.5-dev8 → 0.20.5-dev87

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 (250) hide show
  1. package/dist/{Combination-Du-o_hC9.js → Combination-Dk6JxauT.js} +1 -1
  2. package/dist/{ConnectedDataExplorerComponent-DUS-zJoR.js → ConnectedDataExplorerComponent-pQ4sWAoT.js} +11 -11
  3. package/dist/{_baseIsEqual-5cAxzk6f.js → _baseIsEqual-CvgsjYoW.js} +38 -38
  4. package/dist/{_basePickBy-3JVb5wYm.js → _basePickBy-pTDW2_2A.js} +6 -6
  5. package/dist/{_baseUniq-DSSiFuIJ.js → _baseUniq-BUFhl85h.js} +1 -1
  6. package/dist/{any-language-editor-BL9o7y0_.js → any-language-editor-BIj11a2e.js} +19 -19
  7. package/dist/{architecture-7HQA4BMR-BxkNpYRp.js → architecture-7HQA4BMR-BmtmhGMc.js} +2 -2
  8. package/dist/{architectureDiagram-VXUJARFQ-DrJeyFHq.js → architectureDiagram-VXUJARFQ-Df0FNeBR.js} +14 -14
  9. package/dist/assets/__vite-browser-external-Us1ds95c.js +1 -0
  10. package/dist/assets/{worker-DUYMdbtA.js → worker-D10K3OOz.js} +2 -2
  11. package/dist/{blockDiagram-VD42YOAC-BJrP6qKc.js → blockDiagram-VD42YOAC-DszWqlLz.js} +7 -7
  12. package/dist/{button-KYalaJYu.js → button-DQpBib29.js} +24 -11
  13. package/dist/{c4Diagram-YG6GDRKO-Bo4gytQ5.js → c4Diagram-YG6GDRKO-Dyj8LoUX.js} +4 -4
  14. package/dist/{channel-IWLGkaBE.js → channel-CUFaIkTh.js} +1 -1
  15. package/dist/{check-C50jsehH.js → check-DpqPQmzz.js} +1 -1
  16. package/dist/{chunk-4F5CHEZ2-CxKDFd-t.js → chunk-4F5CHEZ2-CRwwZ2ED.js} +1 -1
  17. package/dist/{chunk-ABZYJK2D-CRwanrkd.js → chunk-ABZYJK2D-7QYXAAhe.js} +1 -1
  18. package/dist/{chunk-ATLVNIR6-CMMCMvOK.js → chunk-ATLVNIR6-pmHPAPSd.js} +1 -1
  19. package/dist/{chunk-B2363JML-e_W7KW1D.js → chunk-B2363JML-BuBMltZc.js} +1 -1
  20. package/dist/{chunk-B4BG7PRW-BNsHrGHG.js → chunk-B4BG7PRW-Dbta9cTX.js} +4 -4
  21. package/dist/{chunk-DI55MBZ5-DQeYbfMV.js → chunk-DI55MBZ5-DyKB35wC.js} +4 -4
  22. package/dist/{chunk-EXTU4WIE-CV_DQeaX.js → chunk-EXTU4WIE-BRFl4iNd.js} +1 -1
  23. package/dist/{chunk-FRFDVMJY-C7q09nvl.js → chunk-FRFDVMJY-Bk2LD5Te.js} +1 -1
  24. package/dist/{chunk-JA3XYJ7Z-Cmt--e0q.js → chunk-JA3XYJ7Z-BkrY9SdL.js} +2 -2
  25. package/dist/{chunk-JZLCHNYA-CkyMJnI9.js → chunk-JZLCHNYA-Bk_Lil-q.js} +4 -4
  26. package/dist/{chunk-N4CR4FBY-BJfHtJbD.js → chunk-N4CR4FBY-f5n6meOd.js} +5 -5
  27. package/dist/{chunk-PL6DKKU2-ChKBqnoD.js → chunk-PL6DKKU2-DiFkzMfM.js} +1 -1
  28. package/dist/{chunk-QN33PNHL-WOLIPUAJ.js → chunk-QN33PNHL-CXfJywHv.js} +1 -1
  29. package/dist/{chunk-QXUST7PY-DYuD50pU.js → chunk-QXUST7PY-D7-26sj3.js} +5 -5
  30. package/dist/{chunk-S3R3BYOJ-CsnX6RKs.js → chunk-S3R3BYOJ-BRT9vd1R.js} +3 -3
  31. package/dist/{chunk-SJTYNZTY-j6_1s5om.js → chunk-SJTYNZTY-BvVkbShU.js} +1 -1
  32. package/dist/{chunk-TCCFYFTB-DdLCbCTn.js → chunk-TCCFYFTB-DqxhgXG0.js} +31 -31
  33. package/dist/{chunk-TQ3KTPDO-CGsUIC73.js → chunk-TQ3KTPDO-CPkEruAA.js} +1 -1
  34. package/dist/{chunk-TZMSLE5B-B3eYTGCw.js → chunk-TZMSLE5B-DSfBOnzx.js} +1 -1
  35. package/dist/{chunk-UMXZTB3W--LdAK3Bv.js → chunk-UMXZTB3W-C4ypIY3V.js} +1 -1
  36. package/dist/{classDiagram-v2-WZHVMYZB-UTw37Gg8.js → classDiagram-2ON5EDUG-DphiMW3Y.js} +10 -10
  37. package/dist/{classDiagram-2ON5EDUG-C7C-oefv.js → classDiagram-v2-WZHVMYZB-BH1x5h4a.js} +10 -10
  38. package/dist/{clone-BJrS4PdE.js → clone-CEQ-pda1.js} +1 -1
  39. package/dist/{constants-D1Tbg_6B.js → constants-CytQ_3LM.js} +3 -3
  40. package/dist/{copy-oc-FcZzt.js → copy-BkBF0Xgk.js} +2 -2
  41. package/dist/{dagre-6UL2VRFP-BgsUhJrV.js → dagre-6UL2VRFP-DGEbtmgU.js} +12 -12
  42. package/dist/{dagre-CyZCGfV_.js → dagre-BVnNvbvD.js} +37 -37
  43. package/dist/{diagram-PSM6KHXK-BIUUOfKo.js → diagram-PSM6KHXK-CG_usglE.js} +15 -15
  44. package/dist/{diagram-QEK2KX5R-BFjolZQv.js → diagram-QEK2KX5R-CtGFEwzJ.js} +13 -13
  45. package/dist/{diagram-S2PKOQOG-4jfkWoZw.js → diagram-S2PKOQOG-ClKAGmbv.js} +13 -13
  46. package/dist/dist-B4MxkKHf.js +8 -0
  47. package/dist/{dist-De9X_Des.js → dist-B9EjSb9T.js} +1 -1
  48. package/dist/{dist-IW_ARJ3S.js → dist-BFxYppVR.js} +4 -4
  49. package/dist/{dist-D7ZGWV_9.js → dist-BGZ7TWS9.js} +3 -3
  50. package/dist/{dist-CwtEWuFb.js → dist-BSfYc7vq.js} +2 -2
  51. package/dist/{dist-DMS81OrU.js → dist-BUrWeMEP.js} +1 -1
  52. package/dist/dist-BYghZv6b.js +5 -0
  53. package/dist/dist-Be-uQhz5.js +6 -0
  54. package/dist/{dist-Ch_JuCvc.js → dist-BpMlUdNO.js} +3 -3
  55. package/dist/{dist-C6z8U-ms.js → dist-Bq5eYK43.js} +2 -2
  56. package/dist/{dist-BFL9TlzD.js → dist-Bq9zYwJs.js} +5 -5
  57. package/dist/{dist-7ZF--V_D.js → dist-C4K7pumm.js} +2 -2
  58. package/dist/{dist-Qjf6pcqK.js → dist-CAKwXCWI.js} +2 -2
  59. package/dist/dist-CB_xf0ju.js +5 -0
  60. package/dist/{dist-BwQHkjA9.js → dist-CDHl2i1x.js} +4 -4
  61. package/dist/dist-CK0qFAbF.js +8 -0
  62. package/dist/{dist-C4XMUaob.js → dist-CPlGUbk-.js} +2 -2
  63. package/dist/{dist-BT6_J2eq.js → dist-CSEWGuDq.js} +7 -2
  64. package/dist/dist-CYEk-qrr.js +8 -0
  65. package/dist/{dist-CYo3w-nC.js → dist-Cl5iM8xL.js} +3 -3
  66. package/dist/dist-CmKoWpMk.js +5 -0
  67. package/dist/{dist-I8MQW60_.js → dist-CseYuPtL.js} +2 -2
  68. package/dist/dist-D1nf4IQl.js +5 -0
  69. package/dist/{dist-CsqiXw7J.js → dist-D4gcY469.js} +2 -2
  70. package/dist/{dist-DUxS2paD.js → dist-D5NMgbbv.js} +2 -2
  71. package/dist/{dist-UYm1IE5s.js → dist-DERtJN02.js} +2 -2
  72. package/dist/{dist-CFToYDWO.js → dist-DEj2X26M.js} +2 -2
  73. package/dist/{dist-BuapEdlD.js → dist-DOoqn-VL.js} +70 -67
  74. package/dist/{dist-BLThQiU4.js → dist-DUretbKK.js} +2 -2
  75. package/dist/{dist-DEFZ7dnD.js → dist-D_-CGmlh.js} +2 -2
  76. package/dist/dist-Df3AcKpt.js +6 -0
  77. package/dist/dist-DgaFHt_I.js +5 -0
  78. package/dist/dist-Dk10C3ui.js +5 -0
  79. package/dist/{dist-D0f6Yrrb.js → dist-DodLQWPg.js} +1 -1
  80. package/dist/dist-DtyPVMHR.js +5 -0
  81. package/dist/{dist-Cb3cLT39.js → dist-HoZO6brh.js} +2 -2
  82. package/dist/{dist-Cqpjy6bK.js → dist-RNGn_-uD.js} +1 -1
  83. package/dist/{dist-BBcqvpvP.js → dist-Ux6dL_VB.js} +1 -1
  84. package/dist/{dist-B8Y11RWn.js → dist-WIWVvdBh.js} +2 -2
  85. package/dist/{dist-CB6qhQ8K.js → dist-gc9KgJuA.js} +1 -1
  86. package/dist/{dist-ovDpXuSB.js → dist-i-ud9aCA.js} +1 -1
  87. package/dist/dist-ko7WnHAO.js +5 -0
  88. package/dist/{dist-BTQbjEKU.js → dist-lNe4i1Nm.js} +1 -1
  89. package/dist/dist-of7gLRFK.js +8 -0
  90. package/dist/{erDiagram-Q2GNP2WA-Cq5Bz5lG.js → erDiagram-Q2GNP2WA-DPMseVVp.js} +10 -10
  91. package/dist/{error-banner-D0tXnwl4.js → error-banner-BctofTCP.js} +2 -2
  92. package/dist/{esm-BxMbHo0y.js → esm-BBkPJL8N.js} +29 -27
  93. package/dist/{flowDiagram-NV44I4VS-6WPJVFl7.js → flowDiagram-NV44I4VS-BpAIFwW7.js} +10 -10
  94. package/dist/{ganttDiagram-JELNMOA3-AfDhh9CI.js → ganttDiagram-JELNMOA3-DXYghZ9C.js} +3 -3
  95. package/dist/{gitGraph-G5XIXVHT-C0o6gecv.js → gitGraph-G5XIXVHT-ChHUSAop.js} +2 -2
  96. package/dist/{gitGraphDiagram-V2S2FVAM-BRSwuj0Q.js → gitGraphDiagram-V2S2FVAM-CBL-7g3_.js} +12 -12
  97. package/dist/{glide-data-editor-ByPNTNVG.js → glide-data-editor-DqxJOnJk.js} +63 -63
  98. package/dist/{graphlib-DZnBMcsX.js → graphlib-D18eZCT4.js} +10 -10
  99. package/dist/hasIn-B9AbGLj3.js +86 -0
  100. package/dist/{info-VBDWY6EO-Bzsods6X.js → info-VBDWY6EO-CwyXEo8E.js} +2 -2
  101. package/dist/{infoDiagram-HS3SLOUP-Cmxo6jKx.js → infoDiagram-HS3SLOUP-BXGbfBss.js} +12 -12
  102. package/dist/{isArrayLikeObject-Btu-i6_P.js → isArrayLikeObject-BrYl-ETg.js} +25 -26
  103. package/dist/{isEmpty-CZvUtYFp.js → isEmpty-C-xMag79.js} +2 -2
  104. package/dist/{isString-CBr7TEb7.js → isString-D-vNYDBA.js} +1 -1
  105. package/dist/{isSymbol-BuQsMXhk.js → isSymbol-Dyt2NSnN.js} +1 -1
  106. package/dist/{journeyDiagram-XKPGCS4Q-CKYr8cSR.js → journeyDiagram-XKPGCS4Q-D5BIjS4N.js} +3 -3
  107. package/dist/{kanban-definition-3W4ZIXB7-DVvAZzQD.js → kanban-definition-3W4ZIXB7-DhDkqxFB.js} +7 -7
  108. package/dist/{label-CV0KYhtH.js → label-BLDcDYdI.js} +6 -6
  109. package/dist/{loader-eJCvvApN.js → loader-DsE3MiYo.js} +2 -2
  110. package/dist/main.js +1673 -1163
  111. package/dist/{memoize-P1T1IGb9.js → memoize-Cs8aS5RW.js} +1 -1
  112. package/dist/merge-NuyC7LN7.js +51 -0
  113. package/dist/{mermaid-COOB_abB.js → mermaid-DkdSmFY8.js} +42 -42
  114. package/dist/{mermaid-parser.core-Cd-wu4tE.js → mermaid-parser.core-OkWZ8nr-.js} +8 -8
  115. package/dist/{min-CMDDtXJP.js → min-ECVRnCdn.js} +30 -30
  116. package/dist/{mindmap-definition-VGOIOE7T-1ExmnvYy.js → mindmap-definition-VGOIOE7T-BxQi78Vl.js} +9 -9
  117. package/dist/{now-BxlRp0OQ.js → now-BC2mX0ZT.js} +1 -1
  118. package/dist/{packet-DYOGHKS2-Bf1CvFco.js → packet-DYOGHKS2-C62XQjZh.js} +2 -2
  119. package/dist/{pie-VRWISCQL-LY_wbqji.js → pie-VRWISCQL-nfAKQJw3.js} +2 -2
  120. package/dist/{pieDiagram-ADFJNKIX-CJlIsdsU.js → pieDiagram-ADFJNKIX-DfSJXUHa.js} +13 -13
  121. package/dist/{purify.es-CyOIw8ru.js → purify.es-DGenX2XH.js} +67 -67
  122. package/dist/{quadrantDiagram-AYHSOK5B-BU78RiaH.js → quadrantDiagram-AYHSOK5B-CAcVWXc-.js} +2 -2
  123. package/dist/{radar-ZZBFDIW7-Ro3iXZCk.js → radar-ZZBFDIW7-lopS8_4j.js} +2 -2
  124. package/dist/{range-Dh0_-r8P.js → range-BKaWvVUE.js} +8 -8
  125. package/dist/reduce-CqQo8ppc.js +275 -0
  126. package/dist/{requirementDiagram-UZGBJVZJ-DACHtrFr.js → requirementDiagram-UZGBJVZJ-BU7dwzFM.js} +9 -9
  127. package/dist/{sankeyDiagram-TZEHDZUN-Bzg7_UWs.js → sankeyDiagram-TZEHDZUN-BVJnR4_b.js} +2 -2
  128. package/dist/{sequenceDiagram-WL72ISMW-agybEe9J.js → sequenceDiagram-WL72ISMW-CQcFQTwX.js} +4 -4
  129. package/dist/{slides-component-B0yK5GXP.js → slides-component-DwvL_HJi.js} +2 -2
  130. package/dist/{spec-Dq_reDGM.js → spec-CbYkiXG3.js} +5 -5
  131. package/dist/{stateDiagram-FKZM4ZOC-DehQAt8g.js → stateDiagram-FKZM4ZOC-Dx9AIGDe.js} +12 -12
  132. package/dist/{stateDiagram-v2-4FDKWEC3-8VzeREl9.js → stateDiagram-v2-4FDKWEC3-BIeUs-Ed.js} +10 -10
  133. package/dist/style.css +1 -1
  134. package/dist/{timeline-definition-IT6M3QCI-CdCfdaCF.js → timeline-definition-IT6M3QCI-D8B3p7ID.js} +2 -2
  135. package/dist/{toNumber-By7s5JC_.js → toNumber-CbZ70FdN.js} +2 -2
  136. package/dist/{toString-Ckpb50uw.js → toString-DbIAWQpF.js} +2 -2
  137. package/dist/{tooltip-CL8m4f9y.js → tooltip-SPkubVH3.js} +3 -3
  138. package/dist/{treemap-GDKQZRPO-DRxfDG65.js → treemap-GDKQZRPO-CkR-5ai2.js} +2 -2
  139. package/dist/{types-BwnzGcE4.js → types-0FB-N7AA.js} +519 -408
  140. package/dist/{uniq-cCc07Q8K.js → uniq-H2E5nMLq.js} +1 -1
  141. package/dist/{useAsyncData-B4hMFGnF.js → useAsyncData-D7-oahg5.js} +1 -1
  142. package/dist/{useDeepCompareMemoize-DuPhOXzr.js → useDeepCompareMemoize-DLS-bHHT.js} +5 -5
  143. package/dist/{useIframeCapabilities-CAt6D2EI.js → useIframeCapabilities-DFGZKWkO.js} +1 -1
  144. package/dist/{useTheme-BNYQnvu-.js → useTheme-D0rdoMBF.js} +6 -5
  145. package/dist/{vega-component-DouPy8AI.js → vega-component-D2knjGgv.js} +10 -10
  146. package/dist/{xychartDiagram-PRI3JC2R-rEm_SIsC.js → xychartDiagram-PRI3JC2R-XO8FiQjU.js} +5 -5
  147. package/package.json +9 -9
  148. package/src/__mocks__/common.ts +41 -8
  149. package/src/__mocks__/requests.ts +1 -0
  150. package/src/components/app-config/ai-config.tsx +10 -0
  151. package/src/components/chat/__tests__/useFileState.test.tsx +2 -3
  152. package/src/components/chat/acp/__tests__/context-utils.test.ts +2 -6
  153. package/src/components/datasources/components.tsx +3 -6
  154. package/src/components/datasources/datasources.tsx +8 -21
  155. package/src/components/editor/__tests__/data-attributes.test.tsx +2 -11
  156. package/src/components/editor/actions/types.ts +6 -1
  157. package/src/components/editor/actions/useNotebookActions.tsx +50 -13
  158. package/src/components/editor/cell/cell-context-menu.tsx +2 -6
  159. package/src/components/editor/chrome/types.ts +17 -0
  160. package/src/components/editor/connections/add-connection-dialog.tsx +27 -2
  161. package/src/components/editor/connections/database/__tests__/__snapshots__/as-code.test.ts.snap +105 -6
  162. package/src/components/editor/connections/database/__tests__/as-code.test.ts +101 -8
  163. package/src/components/editor/connections/database/as-code.ts +115 -25
  164. package/src/components/editor/connections/database/schemas.ts +49 -2
  165. package/src/components/editor/connections/storage/as-code.ts +1 -1
  166. package/src/components/editor/controls/command-palette.tsx +7 -0
  167. package/src/components/editor/controls/keyboard-shortcuts.tsx +3 -1
  168. package/src/components/editor/file-tree/__tests__/requesting-tree.test.ts +2 -3
  169. package/src/components/editor/file-tree/file-explorer.tsx +48 -62
  170. package/src/components/editor/file-tree/file-icons.tsx +132 -0
  171. package/src/components/editor/file-tree/file-viewer.tsx +1 -1
  172. package/src/components/editor/file-tree/tree-actions.tsx +107 -0
  173. package/src/components/editor/file-tree/types.ts +2 -96
  174. package/src/components/editor/header/filename-input.tsx +4 -1
  175. package/src/components/editor/navigation/__tests__/clipboard.test.ts +2 -4
  176. package/src/components/editor/output/console/ConsoleOutput.tsx +51 -2
  177. package/src/components/editor/output/console/__tests__/ConsoleOutput.test.tsx +97 -16
  178. package/src/components/icons/marimo-icons.tsx +2 -2
  179. package/src/components/pages/home-page.tsx +5 -5
  180. package/src/components/storage/__tests__/storage-snippets.test.ts +253 -0
  181. package/src/components/storage/components.tsx +0 -38
  182. package/src/components/storage/storage-file-viewer.tsx +1 -1
  183. package/src/components/storage/storage-inspector.tsx +66 -51
  184. package/src/components/storage/storage-snippets.ts +67 -0
  185. package/src/components/ui/command.tsx +2 -0
  186. package/src/components/ui/links.tsx +1 -0
  187. package/src/core/ai/tools/__tests__/run-cells-tool.test.ts +206 -0
  188. package/src/core/ai/tools/run-cells-tool.ts +75 -40
  189. package/src/core/cells/__tests__/cells.test.ts +62 -0
  190. package/src/core/cells/__tests__/session.test.ts +2 -7
  191. package/src/core/cells/cells.ts +25 -3
  192. package/src/core/cells/ids.ts +2 -1
  193. package/src/core/codemirror/compat/__tests__/jupyter.test.ts +2 -3
  194. package/src/core/codemirror/keymaps/vim.ts +32 -3
  195. package/src/core/codemirror/markdown/__tests__/commands.test.ts +2 -3
  196. package/src/core/config/__tests__/config-schema.test.ts +6 -2
  197. package/src/core/config/config-schema.ts +1 -0
  198. package/src/core/config/feature-flag.tsx +1 -1
  199. package/src/core/dom/ui-element-constants.ts +15 -0
  200. package/src/core/dom/ui-element.ts +3 -2
  201. package/src/core/export/__tests__/hooks.test.ts +3 -10
  202. package/src/core/hotkeys/__tests__/hotkeys.test.ts +64 -1
  203. package/src/core/hotkeys/hotkeys.ts +29 -3
  204. package/src/core/islands/bridge.ts +1 -0
  205. package/src/core/islands/components/web-components.tsx +2 -1
  206. package/src/core/network/__tests__/requests-network.test.ts +17 -0
  207. package/src/core/network/requests-lazy.ts +1 -0
  208. package/src/core/network/requests-network.ts +9 -0
  209. package/src/core/network/requests-static.ts +1 -0
  210. package/src/core/network/requests-toasting.tsx +1 -0
  211. package/src/core/network/types.ts +1 -0
  212. package/src/core/runtime/__tests__/runtime.test.ts +2 -8
  213. package/src/core/storage/__tests__/state.test.ts +1 -0
  214. package/src/core/wasm/bridge.ts +1 -0
  215. package/src/core/websocket/useMarimoKernelConnection.tsx +2 -0
  216. package/src/plugins/impl/DataTablePlugin.tsx +53 -3
  217. package/src/plugins/impl/FileBrowserPlugin.tsx +8 -5
  218. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +2 -11
  219. package/src/plugins/impl/__tests__/DropdownPlugin.test.tsx +2 -11
  220. package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +4 -1
  221. package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +309 -0
  222. package/src/plugins/impl/mpl-interactive/__tests__/mpl-websocket-shim.test.ts +110 -0
  223. package/src/plugins/impl/mpl-interactive/mpl-websocket-shim.ts +57 -0
  224. package/src/plugins/impl/plotly/PlotlyPlugin.tsx +8 -2
  225. package/src/plugins/plugins.ts +2 -0
  226. package/src/utils/__tests__/copy.test.ts +129 -0
  227. package/src/utils/__tests__/download.test.tsx +12 -14
  228. package/src/utils/__tests__/filenames.test.ts +7 -0
  229. package/src/utils/__tests__/smartMatch.test.ts +61 -0
  230. package/src/utils/copy.ts +43 -0
  231. package/src/utils/filenames.ts +3 -0
  232. package/src/utils/smartMatch.ts +62 -0
  233. package/dist/_baseProperty-D1nWkRMz.js +0 -93
  234. package/dist/assets/__vite-browser-external-WSlCcXn_.js +0 -1
  235. package/dist/dist-BAeGo2rp.js +0 -5
  236. package/dist/dist-BqwCMSEa.js +0 -5
  237. package/dist/dist-Bum8FwTO.js +0 -6
  238. package/dist/dist-C0YiOwt_.js +0 -5
  239. package/dist/dist-C2uPv4iU.js +0 -5
  240. package/dist/dist-C5hOLsJN.js +0 -8
  241. package/dist/dist-C9NIAKMs.js +0 -8
  242. package/dist/dist-CCrzTtvk.js +0 -5
  243. package/dist/dist-CFS9i1rS.js +0 -8
  244. package/dist/dist-CyHZuhPH.js +0 -5
  245. package/dist/dist-CzcjWdIk.js +0 -6
  246. package/dist/dist-DaYyUSNC.js +0 -5
  247. package/dist/dist-DpDcJYNh.js +0 -8
  248. package/dist/dist-U_BfxcPn.js +0 -5
  249. package/dist/merge-CGQkMGzr.js +0 -51
  250. package/dist/reduce-BXFHs7IQ.js +0 -268
@@ -0,0 +1,309 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
+
4
+ import { useCallback, useEffect, useRef } from "react";
5
+ import { z } from "zod";
6
+ import { useEventListener } from "@/hooks/useEventListener";
7
+ import { createPlugin } from "@/plugins/core/builder";
8
+ import { MODEL_MANAGER, type Model } from "@/plugins/impl/anywidget/model";
9
+ import type { ModelState, WidgetModelId } from "@/plugins/impl/anywidget/types";
10
+ import type { IPluginProps } from "@/plugins/types";
11
+ import { downloadBlob } from "@/utils/download";
12
+ import { Logger } from "@/utils/Logger";
13
+ import { MplCommWebSocket } from "./mpl-websocket-shim";
14
+
15
+ const MPL_SCOPE_CLASS = "mpl-interactive-figure";
16
+
17
+ interface Data {
18
+ mplJsUrl: string;
19
+ cssUrl: string;
20
+ toolbarImages: Record<string, string>;
21
+ width: number;
22
+ height: number;
23
+ }
24
+
25
+ interface ModelIdRef {
26
+ model_id: WidgetModelId;
27
+ }
28
+
29
+ declare global {
30
+ interface Window {
31
+ mpl: {
32
+ figure: new (
33
+ id: string,
34
+ ws: MplCommWebSocket,
35
+ ondownload: (figure: MplFigure, format: string) => void,
36
+ element: HTMLElement,
37
+ ) => MplFigure;
38
+ toolbar_items: [
39
+ string | null,
40
+ string | null,
41
+ string | null,
42
+ string | null,
43
+ ][];
44
+ };
45
+ }
46
+ }
47
+
48
+ interface MplFigure {
49
+ id: string;
50
+ ws: MplCommWebSocket;
51
+ root: HTMLElement;
52
+ send_message: (type: string, properties: Record<string, unknown>) => void;
53
+ }
54
+
55
+ export const MplInteractivePlugin = createPlugin<ModelIdRef>(
56
+ "marimo-mpl-interactive",
57
+ )
58
+ .withData(
59
+ z.object({
60
+ mplJsUrl: z.string(),
61
+ cssUrl: z.string(),
62
+ toolbarImages: z.record(z.string(), z.string()),
63
+ width: z.number(),
64
+ height: z.number(),
65
+ }),
66
+ )
67
+ .withFunctions({})
68
+ .renderer((props) => <MplInteractiveSlot {...props} />);
69
+
70
+ let mplJsLoading: Promise<void> | null = null;
71
+
72
+ async function ensureMplJs(jsUrl: string): Promise<void> {
73
+ if (window.mpl) {
74
+ return;
75
+ }
76
+ if (mplJsLoading) {
77
+ return mplJsLoading;
78
+ }
79
+ mplJsLoading = new Promise<void>((resolve, reject) => {
80
+ const script = document.createElement("script");
81
+ script.src = jsUrl;
82
+ script.onload = () => resolve();
83
+ script.onerror = () => {
84
+ mplJsLoading = null;
85
+ reject(new Error("Failed to load mpl.js"));
86
+ };
87
+ document.head.append(script);
88
+ });
89
+ return mplJsLoading;
90
+ }
91
+
92
+ /**
93
+ * Patch mpl.js toolbar image references to use inline data URIs.
94
+ *
95
+ * mpl.js sets `icon_img.src = '_images/' + image + '.png'` and
96
+ * `icon_img.srcset = '_images/' + image + '_large.png 2x'`.
97
+ *
98
+ * We observe the container for new <img> elements and rewrite their
99
+ * src/srcset to the inlined base64 data URIs.
100
+ */
101
+ function patchToolbarImages(
102
+ container: HTMLElement,
103
+ toolbarImages: Record<string, string>,
104
+ ): () => void {
105
+ const patchImg = (img: HTMLImageElement) => {
106
+ const src = img.getAttribute("src") || "";
107
+ const match = src.match(/_images\/(.+)\.png$/);
108
+ if (match) {
109
+ const name = match[1];
110
+ const dataUri = toolbarImages[name];
111
+ if (dataUri) {
112
+ img.src = dataUri;
113
+ }
114
+ }
115
+ const srcset = img.getAttribute("srcset") || "";
116
+ const srcsetMatch = srcset.match(/_images\/(.+)\.png\s+2x$/);
117
+ if (srcsetMatch) {
118
+ const name = srcsetMatch[1];
119
+ const dataUri = toolbarImages[name];
120
+ if (dataUri) {
121
+ img.srcset = `${dataUri} 2x`;
122
+ }
123
+ }
124
+ };
125
+
126
+ // Patch any existing images
127
+ for (const img of container.querySelectorAll("img")) {
128
+ patchImg(img);
129
+ }
130
+
131
+ // Observe for new images added by mpl.js
132
+ const observer = new MutationObserver((mutations) => {
133
+ for (const mutation of mutations) {
134
+ for (const node of mutation.addedNodes) {
135
+ if (node instanceof HTMLImageElement) {
136
+ patchImg(node);
137
+ } else if (node instanceof HTMLElement) {
138
+ for (const img of node.querySelectorAll("img")) {
139
+ patchImg(img);
140
+ }
141
+ }
142
+ }
143
+ }
144
+ });
145
+
146
+ observer.observe(container, { childList: true, subtree: true });
147
+ return () => observer.disconnect();
148
+ }
149
+
150
+ function injectCss(container: HTMLElement, cssUrl: string): () => void {
151
+ const link = document.createElement("link");
152
+ link.rel = "stylesheet";
153
+ link.href = cssUrl;
154
+ container.append(link);
155
+ return () => link.remove();
156
+ }
157
+
158
+ const MplInteractiveSlot = (props: IPluginProps<ModelIdRef, Data>) => {
159
+ const { mplJsUrl, cssUrl, toolbarImages, width, height } = props.data;
160
+ const { model_id: modelId } = props.value;
161
+ const containerRef = useRef<HTMLDivElement>(null);
162
+ const figureRef = useRef<MplFigure | null>(null);
163
+ const wsRef = useRef<MplCommWebSocket | null>(null);
164
+
165
+ const setupFigure = useCallback(
166
+ async (container: HTMLElement) => {
167
+ // Load mpl.js globally (only once, via <script src>)
168
+ await ensureMplJs(mplJsUrl);
169
+
170
+ if (!window.mpl) {
171
+ Logger.error("mpl.js failed to load");
172
+ return;
173
+ }
174
+
175
+ // Get the model from MODEL_MANAGER
176
+ let model: Model<ModelState>;
177
+ try {
178
+ model = await MODEL_MANAGER.get(modelId);
179
+ } catch {
180
+ Logger.error("Failed to get model for mpl interactive", modelId);
181
+ return;
182
+ }
183
+
184
+ // Create the fake WebSocket
185
+ const fakeWs = new MplCommWebSocket((msg: unknown) => {
186
+ // Send from frontend → backend via model custom message
187
+ model.send(msg);
188
+ });
189
+ wsRef.current = fakeWs;
190
+
191
+ // Listen for backend → frontend messages via model custom events
192
+ const handleCustomMessage = (
193
+ content: { type: string; data?: unknown; format?: string },
194
+ buffers?: readonly DataView[],
195
+ ) => {
196
+ if (!content) {
197
+ return;
198
+ }
199
+
200
+ if (content.type === "json") {
201
+ fakeWs.receiveJson(content.data);
202
+ } else if (content.type === "binary" && buffers && buffers.length > 0) {
203
+ fakeWs.receiveBinary(buffers[0]);
204
+ } else if (
205
+ content.type === "download" &&
206
+ buffers &&
207
+ buffers.length > 0
208
+ ) {
209
+ const fmt = content.format || "png";
210
+ const dv = buffers[0];
211
+ const ab = dv.buffer.slice(
212
+ dv.byteOffset,
213
+ dv.byteOffset + dv.byteLength,
214
+ ) as ArrayBuffer;
215
+ downloadBlob(
216
+ new Blob([ab], { type: `image/${fmt}` }),
217
+ `figure.${fmt}`,
218
+ );
219
+ }
220
+ };
221
+
222
+ model.on("msg:custom", handleCustomMessage as any);
223
+
224
+ // Create the mpl figure
225
+ const figId = modelId;
226
+ const ondownload = (_figure: MplFigure, format: string) => {
227
+ // Send download request to backend
228
+ model.send({ type: "download", format });
229
+ };
230
+
231
+ const fig = new window.mpl.figure(figId, fakeWs, ondownload, container);
232
+ figureRef.current = fig;
233
+
234
+ // Set the canvas_div to the backend's figure size so the
235
+ // ResizeObserver doesn't trigger an immediate resize cycle.
236
+ // mpl.js creates: fig.root > [titlebar, canvas_div, toolbar]
237
+ const canvasDiv = fig.root.querySelector<HTMLElement>("div[tabindex]");
238
+ if (canvasDiv) {
239
+ canvasDiv.style.width = `${width}px`;
240
+ canvasDiv.style.height = `${height}px`;
241
+ }
242
+
243
+ // Trigger the onopen callback to start communication
244
+ // mpl.js sends initial messages in onopen
245
+ setTimeout(() => {
246
+ fakeWs.onopen?.();
247
+ }, 0);
248
+
249
+ return () => {
250
+ model.off("msg:custom", handleCustomMessage as any);
251
+ fakeWs.close();
252
+ };
253
+ },
254
+ [modelId, mplJsUrl, width, height],
255
+ );
256
+
257
+ useEffect(() => {
258
+ const container = containerRef.current;
259
+ if (!container) {
260
+ return;
261
+ }
262
+
263
+ // Clear any previous content (handles re-render / cell re-run)
264
+ container.innerHTML = "";
265
+
266
+ // Inject CSS
267
+ const removeCss = injectCss(container, cssUrl);
268
+
269
+ // Patch toolbar images
270
+ const removeImageObserver = patchToolbarImages(container, toolbarImages);
271
+
272
+ let cleanup: (() => void) | undefined;
273
+ let cancelled = false;
274
+
275
+ setupFigure(container)
276
+ .then((cleanupFn) => {
277
+ if (cancelled) {
278
+ cleanupFn?.();
279
+ return;
280
+ }
281
+ cleanup = cleanupFn;
282
+ })
283
+ .catch((error) => {
284
+ if (!cancelled) {
285
+ Logger.error("Failed to set up MPL interactive figure", error);
286
+ }
287
+ });
288
+
289
+ return () => {
290
+ cancelled = true;
291
+ removeCss();
292
+ removeImageObserver();
293
+ cleanup?.();
294
+ // Clear DOM on unmount so stale content doesn't linger
295
+ container.innerHTML = "";
296
+ };
297
+ }, [modelId, cssUrl, toolbarImages, setupFigure]);
298
+
299
+ // Re-request figure when tab becomes visible
300
+ useEventListener(document, "visibilitychange", () => {
301
+ const fig = figureRef.current;
302
+ if (!document.hidden && fig?.ws?.readyState === WebSocket.OPEN) {
303
+ fig.send_message("refresh", {});
304
+ }
305
+ });
306
+
307
+ // Must match _MPL_SCOPE in from_mpl_interactive.py
308
+ return <div ref={containerRef} className={MPL_SCOPE_CLASS} />;
309
+ };
@@ -0,0 +1,110 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { MplCommWebSocket } from "../mpl-websocket-shim";
4
+
5
+ describe("MplCommWebSocket", () => {
6
+ it("starts in OPEN state", () => {
7
+ const ws = new MplCommWebSocket(vi.fn());
8
+ expect(ws.readyState).toBe(WebSocket.OPEN);
9
+ });
10
+
11
+ it("send() parses JSON and calls sendFn with parsed object", () => {
12
+ const sendFn = vi.fn();
13
+ const ws = new MplCommWebSocket(sendFn);
14
+
15
+ ws.send(JSON.stringify({ type: "resize", width: 640, height: 480 }));
16
+
17
+ expect(sendFn).toHaveBeenCalledOnce();
18
+ expect(sendFn).toHaveBeenCalledWith({
19
+ type: "resize",
20
+ width: 640,
21
+ height: 480,
22
+ });
23
+ });
24
+
25
+ it("receiveJson() dispatches MessageEvent with JSON string data", () => {
26
+ const ws = new MplCommWebSocket(vi.fn());
27
+ const handler = vi.fn();
28
+ ws.onmessage = handler;
29
+
30
+ ws.receiveJson({ type: "figure_size", size: [640, 480] });
31
+
32
+ expect(handler).toHaveBeenCalledOnce();
33
+ const event: MessageEvent = handler.mock.calls[0][0];
34
+ expect(event).toBeInstanceOf(MessageEvent);
35
+ expect(event.type).toBe("message");
36
+ expect(JSON.parse(event.data as string)).toEqual({
37
+ type: "figure_size",
38
+ size: [640, 480],
39
+ });
40
+ });
41
+
42
+ it("receiveJson() is a no-op if onmessage is not set", () => {
43
+ const ws = new MplCommWebSocket(vi.fn());
44
+ ws.receiveJson({ type: "test" });
45
+ expect(ws.onmessage).toBeNull();
46
+ });
47
+
48
+ it("receiveBinary() dispatches MessageEvent with Blob data", () => {
49
+ const ws = new MplCommWebSocket(vi.fn());
50
+ const handler = vi.fn();
51
+ ws.onmessage = handler;
52
+
53
+ // Simulate a PNG-like binary buffer
54
+ const bytes = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0, 1, 2, 3]);
55
+ const dv = new DataView(bytes.buffer);
56
+
57
+ ws.receiveBinary(dv);
58
+
59
+ expect(handler).toHaveBeenCalledOnce();
60
+ const event: MessageEvent = handler.mock.calls[0][0];
61
+ expect(event).toBeInstanceOf(MessageEvent);
62
+ expect(event.data).toBeInstanceOf(Blob);
63
+ expect((event.data as Blob).size).toBe(8);
64
+ });
65
+
66
+ it("receiveBinary() handles DataView with offset", () => {
67
+ const ws = new MplCommWebSocket(vi.fn());
68
+ const handler = vi.fn();
69
+ ws.onmessage = handler;
70
+
71
+ // Create a DataView that's a slice of a larger buffer
72
+ const fullBuffer = new ArrayBuffer(16);
73
+ const fullView = new Uint8Array(fullBuffer);
74
+ fullView.set([0, 0, 0, 0, 0x89, 0x50, 0x4e, 0x47, 0, 1, 2, 3, 0, 0, 0, 0]);
75
+ const dv = new DataView(fullBuffer, 4, 8);
76
+
77
+ ws.receiveBinary(dv);
78
+
79
+ expect(handler).toHaveBeenCalledOnce();
80
+ const blob = handler.mock.calls[0][0].data as Blob;
81
+ expect(blob.size).toBe(8);
82
+ });
83
+
84
+ it("close() sets readyState to CLOSED and fires onclose", () => {
85
+ const ws = new MplCommWebSocket(vi.fn());
86
+ const closeHandler = vi.fn();
87
+ ws.onclose = closeHandler;
88
+
89
+ ws.close();
90
+
91
+ expect(ws.readyState).toBe(WebSocket.CLOSED);
92
+ expect(closeHandler).toHaveBeenCalledOnce();
93
+ });
94
+
95
+ it("close() does not throw if onclose is not set", () => {
96
+ const ws = new MplCommWebSocket(vi.fn());
97
+ ws.close();
98
+ expect(ws.readyState).toBe(WebSocket.CLOSED);
99
+ });
100
+
101
+ it("onopen can be triggered externally", () => {
102
+ const ws = new MplCommWebSocket(vi.fn());
103
+ const openHandler = vi.fn();
104
+ ws.onopen = openHandler;
105
+
106
+ ws.onopen?.();
107
+
108
+ expect(openHandler).toHaveBeenCalledOnce();
109
+ });
110
+ });
@@ -0,0 +1,57 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ /**
4
+ * Fake WebSocket that routes messages through MarimoComm / MODEL_MANAGER
5
+ * instead of a real network WebSocket.
6
+ *
7
+ * mpl.js expects a WebSocket-like object with:
8
+ * - readyState
9
+ * - send(data: string)
10
+ * - onopen / onmessage / onclose callbacks
11
+ */
12
+ export class MplCommWebSocket {
13
+ readyState: number = WebSocket.OPEN;
14
+ private sendFn: (msg: unknown) => void;
15
+
16
+ onopen: (() => void) | null = null;
17
+ onmessage: ((evt: MessageEvent) => void) | null = null;
18
+ onclose: (() => void) | null = null;
19
+
20
+ constructor(sendFn: (msg: unknown) => void) {
21
+ this.sendFn = sendFn;
22
+ }
23
+
24
+ /**
25
+ * Called by mpl.js to send a message to the backend.
26
+ * mpl.js always sends JSON strings.
27
+ */
28
+ send(data: string): void {
29
+ this.sendFn(JSON.parse(data));
30
+ }
31
+
32
+ /**
33
+ * Called when the backend pushes a JSON message via the model custom event.
34
+ */
35
+ receiveJson(data: unknown): void {
36
+ this.onmessage?.(
37
+ new MessageEvent("message", { data: JSON.stringify(data) }),
38
+ );
39
+ }
40
+
41
+ /**
42
+ * Called when the backend pushes binary data (PNG render) via model custom event.
43
+ */
44
+ receiveBinary(buffer: DataView): void {
45
+ const ab = buffer.buffer.slice(
46
+ buffer.byteOffset,
47
+ buffer.byteOffset + buffer.byteLength,
48
+ ) as ArrayBuffer;
49
+ const blob = new Blob([ab]);
50
+ this.onmessage?.(new MessageEvent("message", { data: blob }));
51
+ }
52
+
53
+ close(): void {
54
+ this.readyState = WebSocket.CLOSED;
55
+ this.onclose?.();
56
+ }
57
+ }
@@ -250,6 +250,7 @@ const STANDARD_POINT_KEYS: string[] = [
250
250
  "lon",
251
251
  "curveNumber",
252
252
  "pointNumber",
253
+ "pointNumbers",
253
254
  "pointIndex",
254
255
  ];
255
256
 
@@ -263,6 +264,8 @@ function extractPoints(
263
264
  let parser: PlotlyTemplateParser | undefined;
264
265
 
265
266
  return points.map((point) => {
267
+ const standardPointFields = pick(point, STANDARD_POINT_KEYS);
268
+
266
269
  // Get the first hovertemplate
267
270
  const hovertemplate = Array.isArray(point.data.hovertemplate)
268
271
  ? point.data.hovertemplate[0]
@@ -271,13 +274,16 @@ function extractPoints(
271
274
  // For chart types with standard point keys (e.g. heatmaps),
272
275
  // or when there's no hovertemplate, pick keys directly from the point.
273
276
  if (!hovertemplate || point.data?.type === "heatmap") {
274
- return pick(point, STANDARD_POINT_KEYS);
277
+ return standardPointFields;
275
278
  }
276
279
 
277
280
  // Update or create a parser
278
281
  parser = parser
279
282
  ? parser.update(hovertemplate)
280
283
  : createParser(hovertemplate);
281
- return parser.parse(point);
284
+ return {
285
+ ...standardPointFields,
286
+ ...parser.parse(point),
287
+ };
282
288
  });
283
289
  }
@@ -26,6 +26,7 @@ import { MatrixPlugin } from "./impl/MatrixPlugin";
26
26
  import { MicrophonePlugin } from "./impl/MicrophonePlugin";
27
27
  import { MultiselectPlugin } from "./impl/MultiselectPlugin";
28
28
  import { MatplotlibPlugin } from "./impl/matplotlib/MatplotlibPlugin";
29
+ import { MplInteractivePlugin } from "./impl/mpl-interactive/MplInteractivePlugin";
29
30
  import { NumberPlugin } from "./impl/NumberPlugin";
30
31
  import { PanelPlugin } from "./impl/panel/PanelPlugin";
31
32
  import { PlotlyPlugin } from "./impl/plotly/PlotlyPlugin";
@@ -92,6 +93,7 @@ export const UI_PLUGINS: IPlugin<any, unknown>[] = [
92
93
  AnyWidgetPlugin,
93
94
  DataEditorPlugin,
94
95
  PanelPlugin,
96
+ MplInteractivePlugin,
95
97
  ];
96
98
 
97
99
  // List of output / layout plugins
@@ -0,0 +1,129 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { copyImageToClipboard, isSafari } from "../copy";
4
+
5
+ describe("isSafari", () => {
6
+ afterEach(() => {
7
+ vi.restoreAllMocks();
8
+ });
9
+
10
+ it("returns true for Safari on macOS", () => {
11
+ vi.spyOn(navigator, "userAgent", "get").mockReturnValue(
12
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15",
13
+ );
14
+ expect(isSafari()).toBe(true);
15
+ });
16
+
17
+ it("returns true for Safari on iOS", () => {
18
+ vi.spyOn(navigator, "userAgent", "get").mockReturnValue(
19
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1",
20
+ );
21
+ expect(isSafari()).toBe(true);
22
+ });
23
+
24
+ it("returns false for Chrome", () => {
25
+ vi.spyOn(navigator, "userAgent", "get").mockReturnValue(
26
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
27
+ );
28
+ expect(isSafari()).toBe(false);
29
+ });
30
+
31
+ it("returns false for Chrome on iOS (CriOS)", () => {
32
+ vi.spyOn(navigator, "userAgent", "get").mockReturnValue(
33
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/120.0.0.0 Mobile/15E148 Safari/604.1",
34
+ );
35
+ expect(isSafari()).toBe(false);
36
+ });
37
+
38
+ it("returns false for Firefox on iOS (FxiOS)", () => {
39
+ vi.spyOn(navigator, "userAgent", "get").mockReturnValue(
40
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/120.0 Mobile/15E148 Safari/604.1",
41
+ );
42
+ expect(isSafari()).toBe(false);
43
+ });
44
+
45
+ it("returns false for Edge on iOS (EdgiOS)", () => {
46
+ vi.spyOn(navigator, "userAgent", "get").mockReturnValue(
47
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) EdgiOS/120.0.0.0 Mobile/15E148 Safari/604.1",
48
+ );
49
+ expect(isSafari()).toBe(false);
50
+ });
51
+
52
+ it("returns false for Firefox on desktop", () => {
53
+ vi.spyOn(navigator, "userAgent", "get").mockReturnValue(
54
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:120.0) Gecko/20100101 Firefox/120.0",
55
+ );
56
+ expect(isSafari()).toBe(false);
57
+ });
58
+ });
59
+
60
+ describe("copyImageToClipboard", () => {
61
+ let writeMock: ReturnType<typeof vi.fn>;
62
+ let clipboardItemSpy: ReturnType<typeof vi.fn>;
63
+
64
+ beforeEach(() => {
65
+ writeMock = vi.fn().mockResolvedValue(undefined);
66
+ Object.assign(navigator, {
67
+ clipboard: { write: writeMock },
68
+ });
69
+
70
+ // ClipboardItem is not available in jsdom, so we mock it
71
+ clipboardItemSpy = vi.fn().mockImplementation((data) => ({ data }));
72
+ vi.stubGlobal("ClipboardItem", clipboardItemSpy);
73
+ });
74
+
75
+ afterEach(() => {
76
+ vi.restoreAllMocks();
77
+ vi.unstubAllGlobals();
78
+ });
79
+
80
+ it("uses blob type from response on non-Safari browsers", async () => {
81
+ vi.spyOn(navigator, "userAgent", "get").mockReturnValue(
82
+ "Mozilla/5.0 Chrome/120.0.0.0 Safari/537.36",
83
+ );
84
+
85
+ const fakeBlob = new Blob(["fake"], { type: "image/jpeg" });
86
+ vi.spyOn(globalThis, "fetch").mockResolvedValue(
87
+ new Response(fakeBlob, {
88
+ headers: { "Content-Type": "image/jpeg" },
89
+ }),
90
+ );
91
+
92
+ await copyImageToClipboard("https://example.com/image.jpg");
93
+
94
+ expect(writeMock).toHaveBeenCalledOnce();
95
+ // Non-Safari path: awaits blob, uses blob.type as key
96
+ const arg = clipboardItemSpy.mock.calls[0][0];
97
+ expect(arg).toHaveProperty("image/jpeg");
98
+ expect(arg["image/jpeg"].type).toBe("image/jpeg");
99
+ });
100
+
101
+ it("uses image/png on Safari", async () => {
102
+ vi.spyOn(navigator, "userAgent", "get").mockReturnValue(
103
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15",
104
+ );
105
+
106
+ const fakeBlob = new Blob(["fake"], { type: "image/png" });
107
+ vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(fakeBlob));
108
+
109
+ await copyImageToClipboard("https://example.com/image.png");
110
+
111
+ expect(writeMock).toHaveBeenCalledOnce();
112
+ // Safari path: uses "image/png" key with a Promise<Blob>
113
+ expect(clipboardItemSpy).toHaveBeenCalledWith({
114
+ "image/png": expect.any(Promise),
115
+ });
116
+ });
117
+
118
+ it("propagates fetch errors", async () => {
119
+ vi.spyOn(navigator, "userAgent", "get").mockReturnValue(
120
+ "Mozilla/5.0 Chrome/120.0.0.0 Safari/537.36",
121
+ );
122
+
123
+ vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("Network error"));
124
+
125
+ await expect(
126
+ copyImageToClipboard("https://example.com/image.png"),
127
+ ).rejects.toThrow("Network error");
128
+ });
129
+ });
@@ -1,5 +1,6 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { Mocks } from "@/__mocks__/common";
3
4
  import type { CellId } from "@/core/cells/ids";
4
5
  import { CellOutputId } from "@/core/cells/ids";
5
6
  import {
@@ -27,26 +28,23 @@ vi.mock("@/core/network/requests", () => ({
27
28
  }));
28
29
 
29
30
  // Mock the toast module
30
- const mockDismiss = vi.fn();
31
- const mockUpdate = vi.fn();
32
- vi.mock("@/components/ui/use-toast", () => ({
33
- toast: vi.fn(() => ({
34
- dismiss: mockDismiss,
35
- update: mockUpdate,
36
- })),
37
- }));
31
+ const { mockDismiss, mockUpdate, toastMock } = vi.hoisted(() => {
32
+ const dismiss = vi.fn();
33
+ const update = vi.fn();
34
+ return {
35
+ mockDismiss: dismiss,
36
+ mockUpdate: update,
37
+ toastMock: { toast: vi.fn(() => ({ dismiss, update })) },
38
+ };
39
+ });
40
+ vi.mock("@/components/ui/use-toast", () => toastMock);
38
41
 
39
42
  // Mock the Spinner component
40
43
  vi.mock("@/components/icons/spinner", () => ({
41
44
  Spinner: () => "MockSpinner",
42
45
  }));
43
46
 
44
- // Mock Logger
45
- vi.mock("@/utils/Logger", () => ({
46
- Logger: {
47
- error: vi.fn(),
48
- },
49
- }));
47
+ vi.mock("@/utils/Logger", () => ({ Logger: Mocks.quietLogger() }));
50
48
 
51
49
  // Mock Filenames
52
50
  vi.mock("@/utils/filenames", () => ({