@marimo-team/islands 0.19.12-dev1 → 0.20.0

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 (202) hide show
  1. package/dist/Combination-Du-o_hC9.js +11897 -0
  2. package/dist/{ConnectedDataExplorerComponent-CkXO-pKy.js → ConnectedDataExplorerComponent-BMiGWK57.js} +19 -18
  3. package/dist/{_baseIsEqual-CBSjxu-D.js → _baseIsEqual-DN5YkPnl.js} +1 -1
  4. package/dist/{_baseProperty-BVGrW_NZ.js → _baseProperty-6juuyX7Z.js} +5 -5
  5. package/dist/{_baseUniq-4lqa8rDi.js → _baseUniq-BlF21ach.js} +1 -1
  6. package/dist/{any-language-editor-t_VsTNa-.js → any-language-editor-1OMbohwD.js} +19 -19
  7. package/dist/architecture-U656AL7Q-Jd2CvPgJ.js +6 -0
  8. package/dist/{architectureDiagram-VXUJARFQ-DmJQhcJb.js → architectureDiagram-VXUJARFQ-DhN0C3Xf.js} +15 -15
  9. package/dist/{blockDiagram-VD42YOAC-CRofISJs.js → blockDiagram-VD42YOAC-DrBkIcbV.js} +7 -7
  10. package/dist/{button-Cy0ElmIm.js → button-KYalaJYu.js} +783 -200
  11. package/dist/{c4Diagram-YG6GDRKO-Deqoag4I.js → c4Diagram-YG6GDRKO-pWt4zmu0.js} +4 -4
  12. package/dist/{channel-CMsnebrL.js → channel-C56Jz8EL.js} +1 -1
  13. package/dist/{check-DkNR52Mm.js → check-C50jsehH.js} +1 -1
  14. package/dist/{chunk-76Q3JFCE-jPuajZH_.js → chunk-76Q3JFCE-CQ6a2yGJ.js} +1 -1
  15. package/dist/{chunk-ABZYJK2D-BGWvKte3.js → chunk-ABZYJK2D-BwNsaa1P.js} +1 -1
  16. package/dist/{chunk-ATLVNIR6-BYZB6C5T.js → chunk-ATLVNIR6-DtFMAawc.js} +1 -1
  17. package/dist/{chunk-B4BG7PRW-CwYUp6Uj.js → chunk-B4BG7PRW-lfWcLlzS.js} +4 -4
  18. package/dist/{chunk-DI55MBZ5-Gyro6dvN.js → chunk-DI55MBZ5-RhhAimfG.js} +4 -4
  19. package/dist/{chunk-EXTU4WIE-BlA7aWEw.js → chunk-EXTU4WIE-Bmo660a9.js} +1 -1
  20. package/dist/{chunk-FPAJGGOC-CduL34ft.js → chunk-FPAJGGOC-quWdfNUB.js} +7 -7
  21. package/dist/{chunk-FWNWRKHM-C-2TI4gt.js → chunk-FWNWRKHM-DmrwhAQr.js} +1 -1
  22. package/dist/{chunk-JA3XYJ7Z-Cm-pccR-.js → chunk-JA3XYJ7Z-n8UTzfok.js} +2 -2
  23. package/dist/{chunk-JZLCHNYA-CoLqqXMe.js → chunk-JZLCHNYA-ChKqHUdB.js} +4 -4
  24. package/dist/{chunk-LBM3YZW2-DWgQiioW.js → chunk-LBM3YZW2-BkxsqkNK.js} +1 -1
  25. package/dist/{chunk-LHMN2FUI-Dj_AHSvI.js → chunk-LHMN2FUI-CgYPnxfN.js} +1 -1
  26. package/dist/{chunk-N4CR4FBY-ByLbY9L-.js → chunk-N4CR4FBY-BxOHGL3P.js} +5 -5
  27. package/dist/{chunk-O7ZBX7Z2-CRZ8i1rP.js → chunk-O7ZBX7Z2-CdpLwOP0.js} +1 -1
  28. package/dist/{chunk-QN33PNHL-eodIiY9F.js → chunk-QN33PNHL-Dda-55xY.js} +1 -1
  29. package/dist/{chunk-QXUST7PY-CuJlDW6A.js → chunk-QXUST7PY-ClIEpoCT.js} +5 -5
  30. package/dist/{chunk-S3R3BYOJ-CwDGYMVf.js → chunk-S3R3BYOJ-C_1SJcWo.js} +3 -3
  31. package/dist/{chunk-S6J4BHB3-DNVMr0_v.js → chunk-S6J4BHB3-zfWjyfUg.js} +1 -1
  32. package/dist/{chunk-T53DSG4Q-BFWkpOX5.js → chunk-T53DSG4Q-Bewz1tiU.js} +1 -1
  33. package/dist/{chunk-TZMSLE5B-DFDr5FCr.js → chunk-TZMSLE5B-XW3duOft.js} +1 -1
  34. package/dist/{classDiagram-2ON5EDUG-BDBX9etk.js → classDiagram-2ON5EDUG-CUJlS_eo.js} +10 -10
  35. package/dist/{classDiagram-v2-WZHVMYZB-Bzj_L_BF.js → classDiagram-v2-WZHVMYZB-BhSPpbkE.js} +10 -10
  36. package/dist/{clone-Cc_6PW77.js → clone-V9hndNcj.js} +1 -1
  37. package/dist/{constants-DrOu5vvd.js → constants-BGRTDzdW.js} +2 -2
  38. package/dist/{copy-DRaXIb_a.js → copy-oc-FcZzt.js} +2 -2
  39. package/dist/{dagre-6UL2VRFP-r2rSdJYL.js → dagre-6UL2VRFP-BArPH353.js} +11 -11
  40. package/dist/{dagre-D2F8UdM6.js → dagre-Dcgyn_Uy.js} +15 -15
  41. package/dist/{diagram-PSM6KHXK-BpxVUe9U.js → diagram-PSM6KHXK-B1xAkr9y.js} +16 -16
  42. package/dist/{diagram-QEK2KX5R-q3dHUcp6.js → diagram-QEK2KX5R-CaoqwzPb.js} +14 -14
  43. package/dist/{diagram-S2PKOQOG-MDBKrxSC.js → diagram-S2PKOQOG-NXCsFLvR.js} +14 -14
  44. package/dist/dist-B8Y11RWn.js +1381 -0
  45. package/dist/dist-BA-HK7pI.js +5 -0
  46. package/dist/dist-BD5GU948.js +5 -0
  47. package/dist/{dist-BfactX3G.js → dist-BGzkWRSl.js} +4 -4
  48. package/dist/dist-BIYmAsND.js +5 -0
  49. package/dist/{dist-CmZYrgd_.js → dist-BUEi7EKT.js} +1 -1
  50. package/dist/{dist-B94MxrQS.js → dist-B_i29Q6L.js} +2 -2
  51. package/dist/dist-BcKTJXJi.js +5 -0
  52. package/dist/dist-BgnrtcWg.js +8 -0
  53. package/dist/{dist-glA_fIK_.js → dist-BoagoQQw.js} +2 -2
  54. package/dist/{dist-C2-m5aEk.js → dist-BswsDM4k.js} +2 -2
  55. package/dist/dist-C1njTlBq.js +5 -0
  56. package/dist/{dist-B2-r9y-0.js → dist-C5QB1NtD.js} +3 -3
  57. package/dist/{dist-Crk9ejOy.js → dist-CD7uLx0M.js} +2 -2
  58. package/dist/{dist-B4tYJP_i.js → dist-CMOy93xY.js} +2 -2
  59. package/dist/dist-CSKHwJYH.js +5 -0
  60. package/dist/dist-CSKKyiIq.js +5 -0
  61. package/dist/{dist-iiugPhCC.js → dist-C_9IMrtt.js} +1 -1
  62. package/dist/{dist-CE43BRmt.js → dist-Cb3iqED3.js} +1 -1
  63. package/dist/{dist-Dit9tk8a.js → dist-CoZ8kKKW.js} +1 -1
  64. package/dist/{dist-B5ATpkxy.js → dist-CrAYcS_4.js} +2 -2
  65. package/dist/dist-CrQ_pOuK.js +6 -0
  66. package/dist/dist-Cskx1daf.js +5 -0
  67. package/dist/dist-D4i0Ef34.js +8 -0
  68. package/dist/{dist-T4g7Sr6e.js → dist-D8EhXZ4S.js} +3 -3
  69. package/dist/{dist-CJrHMxlI.js → dist-DOLQQtWK.js} +3 -3
  70. package/dist/dist-DOcn61TX.js +8 -0
  71. package/dist/{dist-DqJdzAYM.js → dist-Dmr_nXF6.js} +2 -2
  72. package/dist/{dist-yVJ4xE5n.js → dist-DpAbrLuF.js} +5 -5
  73. package/dist/{dist-CcOGT46m.js → dist-DrC0QKFK.js} +1 -1
  74. package/dist/{dist-BYmtF1W6.js → dist-Dv2Phbh5.js} +2 -2
  75. package/dist/dist-DwMejAPB.js +6 -0
  76. package/dist/dist-DzSe1wby.js +8 -0
  77. package/dist/{dist-BbBnU4tG.js → dist-EZFqUJhh.js} +1 -1
  78. package/dist/{dist-Cgf353Ki.js → dist-Ey9hP8-j.js} +1 -1
  79. package/dist/{dist-BLwfpZD-.js → dist-IlWGXVjO.js} +2 -2
  80. package/dist/{dist-DOil6y-3.js → dist-LNp8svLl.js} +4 -4
  81. package/dist/{dist-CPTE45iS.js → dist-W6TdeACj.js} +1 -1
  82. package/dist/{dist-Dc1SFk5I.js → dist-a6Obzr07.js} +2 -2
  83. package/dist/{dist-Bsv_ARko.js → dist-bz6WguLy.js} +2 -2
  84. package/dist/{dist-CC9VUnXd.js → dist-iDeoXzdN.js} +1 -1
  85. package/dist/{dist-BoAHOW2l.js → dist-iyBCcLRa.js} +2 -2
  86. package/dist/{dist-CkEUrAus.js → dist-xCB683Dh.js} +2 -2
  87. package/dist/{erDiagram-Q2GNP2WA-CX1XdqVD.js → erDiagram-Q2GNP2WA-DWCa11g5.js} +10 -10
  88. package/dist/error-banner-vCG-EbUQ.js +619 -0
  89. package/dist/{esm-BAS2d2Ad.js → esm-DZSk8vt3.js} +27 -27
  90. package/dist/{flatten-eGRGXrC3.js → flatten-CWZjF1fc.js} +1 -1
  91. package/dist/{flowDiagram-NV44I4VS-BCj-ONTw.js → flowDiagram-NV44I4VS-BQ5PQs4L.js} +10 -10
  92. package/dist/{ganttDiagram-JELNMOA3-D1l5ewiQ.js → ganttDiagram-JELNMOA3-NTOuNWeT.js} +3 -3
  93. package/dist/{gitGraph-F6HP7TQM-CDM3aU-T.js → gitGraph-F6HP7TQM-DfRNsaDw.js} +3 -3
  94. package/dist/{gitGraphDiagram-NY62KEGX-KdZh0iiW.js → gitGraphDiagram-NY62KEGX-CYke62Ot.js} +13 -13
  95. package/dist/{glide-data-editor-2RvcPqmc.js → glide-data-editor-DttqGjrT.js} +571 -572
  96. package/dist/{graphlib-7UgfJadv.js → graphlib-CwMnCnQ9.js} +8 -8
  97. package/dist/{info-NVLQJR56-CoL1x1Fy.js → info-NVLQJR56-CUaoPtis.js} +3 -3
  98. package/dist/{infoDiagram-WHAUD3N6-PSH7lQ0D.js → infoDiagram-WHAUD3N6-B42WjAPh.js} +13 -13
  99. package/dist/{isEmpty-DQXRKNtW.js → isEmpty-6z2uv6gM.js} +2 -2
  100. package/dist/{isString-Clqvtgmo.js → isString-D6abkXrl.js} +1 -1
  101. package/dist/{isSymbol-TWXhTa8k.js → isSymbol-hk7foJ70.js} +1 -1
  102. package/dist/{journeyDiagram-XKPGCS4Q-BrTAxQ1J.js → journeyDiagram-XKPGCS4Q-ahXD97kr.js} +3 -3
  103. package/dist/{kanban-definition-3W4ZIXB7-BoYCDp_9.js → kanban-definition-3W4ZIXB7-CiTIpnhy.js} +7 -7
  104. package/dist/{label-CxU5JNBW.js → label-Cc5tEavt.js} +250 -250
  105. package/dist/{loader-C0-eIoas.js → loader-Cob3XFOw.js} +2 -2
  106. package/dist/main.js +1791 -1056
  107. package/dist/{memoize-Bag7B41I.js → memoize-Ckyqzyu_.js} +1 -1
  108. package/dist/{merge-Dl1bfxsj.js → merge-Db4Uulx4.js} +1 -1
  109. package/dist/{mermaid-C2cSe5YL.js → mermaid-B5xl_2hx.js} +73 -62
  110. package/dist/{mermaid-parser.core-D20zFbMa.js → mermaid-parser.core-BXj7Il0J.js} +8 -8
  111. package/dist/{min-Bg4bqmiD.js → min-ypdVXicC.js} +4 -4
  112. package/dist/{mindmap-definition-VGOIOE7T-CmRjsKEt.js → mindmap-definition-VGOIOE7T-Mni766A_.js} +9 -9
  113. package/dist/{now-mivqkCIv.js → now-Dwu5ou19.js} +2 -2
  114. package/dist/{once-BqS42WgZ.js → once-C9dA9qgQ.js} +1 -1
  115. package/dist/{packet-BFZMPI3H-C6aZmgV-.js → packet-BFZMPI3H-DHtQCusE.js} +3 -3
  116. package/dist/{pie-7BOR55EZ-NB6xYwcB.js → pie-7BOR55EZ-2sVLYbpR.js} +3 -3
  117. package/dist/{pieDiagram-ADFJNKIX-CtxQlnsU.js → pieDiagram-ADFJNKIX-PbXpgT8_.js} +14 -14
  118. package/dist/{quadrantDiagram-AYHSOK5B-DllnB2Hl.js → quadrantDiagram-AYHSOK5B-BtXGnx8i.js} +2 -2
  119. package/dist/{radar-NHE76QYJ-RKhErikV.js → radar-NHE76QYJ-Be0pEUux.js} +3 -3
  120. package/dist/{range-LoQMRQIX.js → range-D9jxVFd_.js} +5 -5
  121. package/dist/{reduce-B9mZDxPo.js → reduce-C6NEPj6s.js} +4 -4
  122. package/dist/{requirementDiagram-UZGBJVZJ-D36MI1k0.js → requirementDiagram-UZGBJVZJ-DxzXQRgq.js} +9 -9
  123. package/dist/{sankeyDiagram-TZEHDZUN-D1mygNPC.js → sankeyDiagram-TZEHDZUN-D-I7dJ0_.js} +2 -2
  124. package/dist/{sequenceDiagram-WL72ISMW-CWdn91Rf.js → sequenceDiagram-WL72ISMW-VDme2ljw.js} +4 -4
  125. package/dist/{slides-component-DfwLApNr.js → slides-component-ql7-5GDI.js} +2 -2
  126. package/dist/{spec-HoYHAQo2.js → spec-GwhMEXwK.js} +8 -9
  127. package/dist/{stateDiagram-FKZM4ZOC-CPxroWXd.js → stateDiagram-FKZM4ZOC-g3GI1EcK.js} +12 -12
  128. package/dist/{stateDiagram-v2-4FDKWEC3-BpM9Q54b.js → stateDiagram-v2-4FDKWEC3-7i6jBXe6.js} +10 -10
  129. package/dist/stex-D2rme5UG.js +4 -0
  130. package/dist/style.css +1 -1
  131. package/dist/{timeline-definition-IT6M3QCI-CVnRHx_t.js → timeline-definition-IT6M3QCI-bhvLlX_b.js} +2 -2
  132. package/dist/{toString-C4TLO6FA.js → toString-BwTJvlyD.js} +2 -2
  133. package/dist/tooltip-CL8m4f9y.js +404 -0
  134. package/dist/{treemap-KMMF4GRG-B37ugcLd.js → treemap-KMMF4GRG-Ba9ifjpG.js} +3 -3
  135. package/dist/{types-Ckva8JJq.js → types-Dsh6yC4B.js} +412 -413
  136. package/dist/{useAsyncData-dr8GazGv.js → useAsyncData-BPpyKjTJ.js} +2 -2
  137. package/dist/{useDeepCompareMemoize-ChviuF5n.js → useDeepCompareMemoize-C8Ms87P-.js} +18 -19
  138. package/dist/{useIframeCapabilities-DurI5SJh.js → useIframeCapabilities-C7z8VrZ1.js} +2 -2
  139. package/dist/{useTheme-SlKl8MlS.js → useTheme-Cq-gIssy.js} +299 -300
  140. package/dist/{vega-component-CnG0vAjf.js → vega-component-B5sxdjMq.js} +10 -10
  141. package/dist/{xychartDiagram-PRI3JC2R-BltwMWKC.js → xychartDiagram-PRI3JC2R-CFxuifYY.js} +5 -5
  142. package/package.json +1 -1
  143. package/src/components/editor/Output.tsx +8 -6
  144. package/src/components/editor/__tests__/Output.test.tsx +59 -0
  145. package/src/components/editor/chrome/__tests__/state.test.ts +321 -0
  146. package/src/components/editor/chrome/state.ts +27 -2
  147. package/src/components/editor/file-tree/upload.tsx +46 -23
  148. package/src/components/editor/links/cell-link.tsx +3 -2
  149. package/src/components/editor/output/console/ConsoleOutput.tsx +13 -3
  150. package/src/components/pages/gallery-page.tsx +1 -1
  151. package/src/components/pages/home-page.tsx +5 -3
  152. package/src/components/tracing/tracing.tsx +50 -39
  153. package/src/core/documentation/DocHoverTarget.tsx +23 -0
  154. package/src/core/documentation/doc-lookup.ts +50 -0
  155. package/src/core/islands/main.ts +1 -0
  156. package/src/core/websocket/useMarimoKernelConnection.tsx +3 -0
  157. package/src/css/app/Cell.css +5 -0
  158. package/src/mount.tsx +2 -2
  159. package/src/plugins/core/RenderHTML.tsx +15 -0
  160. package/src/plugins/core/__test__/registerReactComponent.test.ts +204 -0
  161. package/src/plugins/core/registerReactComponent.tsx +33 -0
  162. package/src/plugins/impl/MatrixPlugin.tsx +275 -0
  163. package/src/plugins/impl/__tests__/MatrixPlugin.test.tsx +415 -0
  164. package/src/plugins/impl/anywidget/model.ts +1 -2
  165. package/src/plugins/impl/matplotlib/MatplotlibPlugin.tsx +70 -0
  166. package/src/plugins/impl/matplotlib/__tests__/matplotlib-renderer.test.ts +152 -0
  167. package/src/plugins/impl/matplotlib/matplotlib-renderer.ts +781 -0
  168. package/src/plugins/impl/matrix.css +45 -0
  169. package/src/plugins/layout/mermaid/mermaid.tsx +11 -3
  170. package/src/plugins/plugins.ts +4 -0
  171. package/src/utils/__tests__/download.test.tsx +47 -0
  172. package/src/utils/download.ts +13 -1
  173. package/src/utils/links.ts +1 -1
  174. package/src/utils/urls.ts +1 -1
  175. package/dist/Combination-BTMrlhzT.js +0 -2611
  176. package/dist/architecture-U656AL7Q-COfwZju8.js +0 -6
  177. package/dist/dist-4YNZxwMI.js +0 -8
  178. package/dist/dist-7nR3r2kG.js +0 -5
  179. package/dist/dist-B2gkyT3r.js +0 -5
  180. package/dist/dist-B8G3I6vJ.js +0 -8
  181. package/dist/dist-BJ96Ykfp.js +0 -8
  182. package/dist/dist-BKLIWGw4.js +0 -5
  183. package/dist/dist-Bf3ou00A.js +0 -6
  184. package/dist/dist-BvkKXuPm.js +0 -5
  185. package/dist/dist-C6NJ3n6r.js +0 -5
  186. package/dist/dist-CecLPYY5.js +0 -5
  187. package/dist/dist-Ch0SwRzK.js +0 -5
  188. package/dist/dist-D6eWHiFh.js +0 -6
  189. package/dist/dist-DCQ710Bv.js +0 -5
  190. package/dist/dist-P_pkS5f-.js +0 -8
  191. package/dist/error-banner-D2zjeN_a.js +0 -1015
  192. package/dist/hotkeys-B5WnGZXF.js +0 -587
  193. package/dist/stex-ChDHQs3R.js +0 -4
  194. package/dist/zod-bjADtMKr.js +0 -10663
  195. /package/dist/{_arrayReduce-DlK7U3Q6.js → _arrayReduce-REKcIEj3.js} +0 -0
  196. /package/dist/{_baseFor-DSVmVciX.js → _baseFor-B69PDbIz.js} +0 -0
  197. /package/dist/{_hasUnicode-Bz2x6u6r.js → _hasUnicode-DrSAc5A5.js} +0 -0
  198. /package/dist/{dist-r8ecBV-v.js → dist-CUOuFgHt.js} +0 -0
  199. /package/dist/{invariant-D9QLJ4SZ.js → invariant-D-K49MfV.js} +0 -0
  200. /package/dist/{main-DhFbkwoC.js → main-DmxVpB19.js} +0 -0
  201. /package/dist/{purify.es-Brw-U87Q.js → purify.es-D4vaFt5N.js} +0 -0
  202. /package/dist/{stex-DrxP7bb3.js → stex-DIvyJfNO.js} +0 -0
@@ -0,0 +1,275 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { type JSX, useCallback, useRef, useState } from "react";
3
+ import { z } from "zod";
4
+ import { cn } from "@/utils/cn";
5
+ import type { IPlugin, IPluginProps, Setter } from "../types";
6
+ import { Labeled } from "./common/labeled";
7
+ import matrixCss from "./matrix.css?inline";
8
+
9
+ type T = number[][];
10
+
11
+ interface Data {
12
+ label: string | null;
13
+ minValue?: number[][] | null;
14
+ maxValue?: number[][] | null;
15
+ step: number[][];
16
+ precision: number;
17
+ rowLabels?: string[] | null;
18
+ columnLabels?: string[] | null;
19
+ symmetric: boolean;
20
+ debounce: boolean;
21
+ scientific: boolean;
22
+ disabled: boolean[][];
23
+ }
24
+
25
+ export class MatrixPlugin implements IPlugin<T, Data> {
26
+ tagName = "marimo-matrix";
27
+
28
+ cssStyles = [matrixCss];
29
+
30
+ validator = z.object({
31
+ initialValue: z.array(z.array(z.number())),
32
+ label: z.string().nullable(),
33
+ minValue: z.array(z.array(z.number())).nullish(),
34
+ maxValue: z.array(z.array(z.number())).nullish(),
35
+ step: z.array(z.array(z.number())),
36
+ precision: z.number(),
37
+ rowLabels: z.array(z.string()).nullish(),
38
+ columnLabels: z.array(z.string()).nullish(),
39
+ symmetric: z.boolean(),
40
+ debounce: z.boolean().default(false),
41
+ scientific: z.boolean(),
42
+ disabled: z.array(z.array(z.boolean())),
43
+ });
44
+
45
+ render(props: IPluginProps<T, Data>): JSX.Element {
46
+ return (
47
+ <MatrixComponent
48
+ {...props.data}
49
+ value={props.value}
50
+ setValue={props.setValue}
51
+ />
52
+ );
53
+ }
54
+ }
55
+
56
+ const PIXELS_PER_STEP = 10;
57
+
58
+ interface MatrixComponentProps extends Data {
59
+ value: T;
60
+ setValue: Setter<T>;
61
+ }
62
+
63
+ const MatrixComponent = ({
64
+ value,
65
+ setValue,
66
+ label,
67
+ minValue,
68
+ maxValue,
69
+ step,
70
+ precision,
71
+ rowLabels,
72
+ columnLabels,
73
+ symmetric,
74
+ debounce,
75
+ scientific,
76
+ disabled,
77
+ }: MatrixComponentProps): JSX.Element => {
78
+ const dragState = useRef<{
79
+ row: number;
80
+ col: number;
81
+ startX: number;
82
+ startValue: number;
83
+ } | null>(null);
84
+ const [activeCell, setActiveCell] = useState<{
85
+ row: number;
86
+ col: number;
87
+ } | null>(null);
88
+
89
+ // Draft holds local edits during an active drag/interaction.
90
+ // Outside of a drag we always read from the prop `value` directly,
91
+ // which avoids stale-state bugs when the matrix shape changes.
92
+ const [draft, setDraft] = useState(value);
93
+ const displayValue = activeCell != null ? draft : value;
94
+
95
+ const formatValue = (val: number) =>
96
+ scientific ? val.toExponential(precision) : val.toFixed(precision);
97
+
98
+ const clampValue = useCallback(
99
+ (val: number, row: number, col: number): number => {
100
+ let clamped = val;
101
+ if (minValue != null) {
102
+ clamped = Math.max(clamped, minValue[row][col]);
103
+ }
104
+ if (maxValue != null) {
105
+ clamped = Math.min(clamped, maxValue[row][col]);
106
+ }
107
+ return clamped;
108
+ },
109
+ [minValue, maxValue],
110
+ );
111
+
112
+ const handlePointerDown = useCallback(
113
+ (e: React.PointerEvent, row: number, col: number) => {
114
+ if (disabled[row][col] || !(e.target instanceof Element)) {
115
+ return;
116
+ }
117
+ e.preventDefault();
118
+ e.target.setPointerCapture(e.pointerId);
119
+ dragState.current = {
120
+ row,
121
+ col,
122
+ startX: e.clientX,
123
+ startValue: displayValue[row][col],
124
+ };
125
+ setActiveCell({ row, col });
126
+ },
127
+ [disabled, displayValue],
128
+ );
129
+
130
+ const handlePointerMove = useCallback(
131
+ (e: React.PointerEvent) => {
132
+ const state = dragState.current;
133
+ if (!state) {
134
+ return;
135
+ }
136
+ const { row, col, startX, startValue } = state;
137
+ const dx = e.clientX - startX;
138
+ const cellStep = step[row][col];
139
+ const steps = Math.round(dx / PIXELS_PER_STEP);
140
+ const rawValue = startValue + steps * cellStep;
141
+ const newValue = clampValue(rawValue, row, col);
142
+
143
+ if (newValue !== displayValue[row][col]) {
144
+ const copy = displayValue.map((r) => [...r]);
145
+ copy[row][col] = newValue;
146
+ if (symmetric && row !== col) {
147
+ copy[col][row] = newValue;
148
+ }
149
+ setDraft(copy);
150
+ if (!debounce) {
151
+ setValue(copy);
152
+ }
153
+ }
154
+ },
155
+ [step, clampValue, displayValue, symmetric, debounce, setValue],
156
+ );
157
+
158
+ const handlePointerUp = useCallback(() => {
159
+ if (debounce && dragState.current) {
160
+ setValue(displayValue);
161
+ }
162
+ dragState.current = null;
163
+ setActiveCell(null);
164
+ }, [debounce, displayValue, setValue]);
165
+
166
+ const handleKeyDown = useCallback(
167
+ (e: React.KeyboardEvent, row: number, col: number) => {
168
+ if (e.key === "ArrowUp" || e.key === "ArrowDown") {
169
+ if (disabled[row][col]) {
170
+ return;
171
+ }
172
+ e.preventDefault();
173
+ const cellStep = step[row][col];
174
+ const delta = e.key === "ArrowUp" ? cellStep : -cellStep;
175
+ const newValue = clampValue(displayValue[row][col] + delta, row, col);
176
+
177
+ if (newValue !== displayValue[row][col]) {
178
+ const copy = displayValue.map((r) => [...r]);
179
+ copy[row][col] = newValue;
180
+ if (symmetric && row !== col) {
181
+ copy[col][row] = newValue;
182
+ }
183
+ setDraft(copy);
184
+ setValue(copy);
185
+ }
186
+ }
187
+ },
188
+ [disabled, step, displayValue, clampValue, symmetric, setValue],
189
+ );
190
+
191
+ const hasRowLabels = rowLabels != null && rowLabels.length > 0;
192
+ const hasColumnLabels = columnLabels != null && columnLabels.length > 0;
193
+
194
+ const numRows = displayValue.length;
195
+ const numCols = displayValue[0]?.length ?? 0;
196
+
197
+ return (
198
+ <Labeled label={label} align="top" className="items-center">
199
+ <div
200
+ className="relative inline-block"
201
+ data-testid="marimo-plugin-matrix"
202
+ onPointerMove={handlePointerMove}
203
+ onPointerUp={handlePointerUp}
204
+ onPointerCancel={handlePointerUp}
205
+ >
206
+ <table
207
+ className="font-mono text-sm tabular-nums select-none border-separate border-spacing-0"
208
+ role="group"
209
+ aria-label={label || "Matrix"}
210
+ >
211
+ {hasColumnLabels && (
212
+ <thead>
213
+ <tr>
214
+ {hasRowLabels && <th />}
215
+ {columnLabels.map((lbl, j) => (
216
+ <th
217
+ key={j}
218
+ className="text-center text-sm font-medium text-foreground px-2 pb-1"
219
+ >
220
+ {lbl}
221
+ </th>
222
+ ))}
223
+ </tr>
224
+ </thead>
225
+ )}
226
+ <tbody>
227
+ {displayValue.map((row, i) => (
228
+ <tr key={i}>
229
+ {hasRowLabels && (
230
+ <th className="text-right text-sm font-medium text-foreground pr-3 h-8">
231
+ {rowLabels[i]}
232
+ </th>
233
+ )}
234
+ {row.map((cellValue, j) => {
235
+ const isDisabled = disabled[i][j];
236
+ const isActive =
237
+ activeCell?.row === i && activeCell?.col === j;
238
+ const rowLabel = rowLabels?.[i] ?? `Row ${i + 1}`;
239
+ const colLabel = columnLabels?.[j] ?? `Column ${j + 1}`;
240
+ return (
241
+ <td
242
+ key={j}
243
+ className={cn(
244
+ "relative text-center min-w-14 h-8 px-2 transition-colors touch-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none",
245
+ isDisabled
246
+ ? "cursor-default text-muted-foreground"
247
+ : "cursor-ew-resize text-[var(--link)] hover:bg-accent",
248
+ isActive && "bg-accent",
249
+ j === 0 && "bracket-l",
250
+ j === numCols - 1 && "bracket-r",
251
+ i === 0 && "bracket-t",
252
+ i === numRows - 1 && "bracket-b",
253
+ )}
254
+ tabIndex={isDisabled ? -1 : 0}
255
+ aria-label={`${rowLabel}, ${colLabel}`}
256
+ aria-valuenow={cellValue}
257
+ aria-valuemin={minValue?.[i]?.[j]}
258
+ aria-valuemax={maxValue?.[i]?.[j]}
259
+ aria-disabled={isDisabled || undefined}
260
+ onPointerDown={(e) => handlePointerDown(e, i, j)}
261
+ onKeyDown={(e) => handleKeyDown(e, i, j)}
262
+ data-testid={`matrix-cell-${i}-${j}`}
263
+ >
264
+ {formatValue(cellValue)}
265
+ </td>
266
+ );
267
+ })}
268
+ </tr>
269
+ ))}
270
+ </tbody>
271
+ </table>
272
+ </div>
273
+ </Labeled>
274
+ );
275
+ };
@@ -0,0 +1,415 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { fireEvent, render } from "@testing-library/react";
4
+ import { beforeEach, describe, expect, it, vi } from "vitest";
5
+ import type { IPluginProps } from "../../types";
6
+ import { MatrixPlugin } from "../MatrixPlugin";
7
+
8
+ type PluginData = ReturnType<MatrixPlugin["validator"]["parse"]>;
9
+
10
+ function makeProps(
11
+ overrides: Partial<IPluginProps<number[][], PluginData>> = {},
12
+ ): IPluginProps<number[][], PluginData> {
13
+ return {
14
+ host: document.createElement("div"),
15
+ value: [
16
+ [1, 2],
17
+ [3, 4],
18
+ ],
19
+ setValue: vi.fn(),
20
+ data: {
21
+ initialValue: [
22
+ [1, 2],
23
+ [3, 4],
24
+ ],
25
+ label: null,
26
+ minValue: null,
27
+ maxValue: null,
28
+ step: [
29
+ [1, 1],
30
+ [1, 1],
31
+ ],
32
+ precision: 1,
33
+ rowLabels: null,
34
+ columnLabels: null,
35
+ symmetric: false,
36
+ debounce: false,
37
+ scientific: false,
38
+ disabled: [
39
+ [false, false],
40
+ [false, false],
41
+ ],
42
+ },
43
+ functions: {},
44
+ ...overrides,
45
+ };
46
+ }
47
+
48
+ beforeEach(() => {
49
+ // jsdom doesn't implement pointer capture
50
+ Element.prototype.setPointerCapture = vi.fn();
51
+ Element.prototype.releasePointerCapture = vi.fn();
52
+
53
+ // jsdom's PointerEvent doesn't properly inherit MouseEvent properties
54
+ // like clientX. Polyfill it so fireEvent.pointerDown/Move/Up work.
55
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
56
+ (globalThis as any).PointerEvent = class PointerEvent extends MouseEvent {
57
+ readonly pointerId: number;
58
+ constructor(type: string, init: PointerEventInit = {}) {
59
+ super(type, init);
60
+ this.pointerId = init.pointerId ?? 0;
61
+ }
62
+ };
63
+ });
64
+
65
+ describe("MatrixPlugin", () => {
66
+ it("renders correct number of cells", () => {
67
+ const plugin = new MatrixPlugin();
68
+ const props = makeProps();
69
+ const { getAllByTestId } = render(plugin.render(props));
70
+
71
+ const cells = getAllByTestId(/^matrix-cell-/);
72
+ expect(cells).toHaveLength(4);
73
+ });
74
+
75
+ it("displays values with correct precision", () => {
76
+ const plugin = new MatrixPlugin();
77
+ const props = makeProps({
78
+ value: [
79
+ [1.5, 2.123],
80
+ [3, 4.9],
81
+ ],
82
+ data: {
83
+ ...makeProps().data,
84
+ precision: 2,
85
+ },
86
+ });
87
+ const { getByTestId } = render(plugin.render(props));
88
+
89
+ expect(getByTestId("matrix-cell-0-0").textContent).toBe("1.50");
90
+ expect(getByTestId("matrix-cell-0-1").textContent).toBe("2.12");
91
+ expect(getByTestId("matrix-cell-1-0").textContent).toBe("3.00");
92
+ expect(getByTestId("matrix-cell-1-1").textContent).toBe("4.90");
93
+ });
94
+
95
+ it("renders disabled cells with aria-disabled", () => {
96
+ const plugin = new MatrixPlugin();
97
+ const props = makeProps({
98
+ data: {
99
+ ...makeProps().data,
100
+ disabled: [
101
+ [true, false],
102
+ [false, true],
103
+ ],
104
+ },
105
+ });
106
+ const { getByTestId } = render(plugin.render(props));
107
+
108
+ expect(getByTestId("matrix-cell-0-0").getAttribute("aria-disabled")).toBe(
109
+ "true",
110
+ );
111
+ expect(getByTestId("matrix-cell-0-1").hasAttribute("aria-disabled")).toBe(
112
+ false,
113
+ );
114
+ expect(getByTestId("matrix-cell-1-0").hasAttribute("aria-disabled")).toBe(
115
+ false,
116
+ );
117
+ expect(getByTestId("matrix-cell-1-1").getAttribute("aria-disabled")).toBe(
118
+ "true",
119
+ );
120
+ });
121
+
122
+ it("renders row labels", () => {
123
+ const plugin = new MatrixPlugin();
124
+ const props = makeProps({
125
+ data: {
126
+ ...makeProps().data,
127
+ rowLabels: ["Row A", "Row B"],
128
+ },
129
+ });
130
+ const { getByText } = render(plugin.render(props));
131
+
132
+ expect(getByText("Row A")).toBeDefined();
133
+ expect(getByText("Row B")).toBeDefined();
134
+ });
135
+
136
+ it("renders column labels", () => {
137
+ const plugin = new MatrixPlugin();
138
+ const props = makeProps({
139
+ data: {
140
+ ...makeProps().data,
141
+ columnLabels: ["Col X", "Col Y"],
142
+ },
143
+ });
144
+ const { getByText } = render(plugin.render(props));
145
+
146
+ expect(getByText("Col X")).toBeDefined();
147
+ expect(getByText("Col Y")).toBeDefined();
148
+ });
149
+
150
+ it("renders a 3x3 matrix", () => {
151
+ const plugin = new MatrixPlugin();
152
+ const props = makeProps({
153
+ value: [
154
+ [1, 0, 0],
155
+ [0, 1, 0],
156
+ [0, 0, 1],
157
+ ],
158
+ data: {
159
+ ...makeProps().data,
160
+ step: [
161
+ [1, 1, 1],
162
+ [1, 1, 1],
163
+ [1, 1, 1],
164
+ ],
165
+ disabled: [
166
+ [false, false, false],
167
+ [false, false, false],
168
+ [false, false, false],
169
+ ],
170
+ },
171
+ });
172
+ const { getAllByTestId } = render(plugin.render(props));
173
+
174
+ const cells = getAllByTestId(/^matrix-cell-/);
175
+ expect(cells).toHaveLength(9);
176
+ });
177
+
178
+ it("validates with zod schema", () => {
179
+ const plugin = new MatrixPlugin();
180
+ const result = plugin.validator.safeParse({
181
+ initialValue: [
182
+ [1, 2],
183
+ [3, 4],
184
+ ],
185
+ label: "test",
186
+ minValue: null,
187
+ maxValue: null,
188
+ step: [
189
+ [1, 1],
190
+ [1, 1],
191
+ ],
192
+ precision: 1,
193
+ rowLabels: null,
194
+ columnLabels: null,
195
+ symmetric: false,
196
+ scientific: false,
197
+ disabled: [
198
+ [false, false],
199
+ [false, false],
200
+ ],
201
+ });
202
+ expect(result.success).toBe(true);
203
+ });
204
+
205
+ it("displays values in scientific notation", () => {
206
+ const plugin = new MatrixPlugin();
207
+ const props = makeProps({
208
+ value: [
209
+ [0.00153, 1234567],
210
+ [0, -0.042],
211
+ ],
212
+ data: {
213
+ ...makeProps().data,
214
+ scientific: true,
215
+ precision: 2,
216
+ },
217
+ });
218
+ const { getByTestId } = render(plugin.render(props));
219
+
220
+ expect(getByTestId("matrix-cell-0-0").textContent).toBe("1.53e-3");
221
+ expect(getByTestId("matrix-cell-0-1").textContent).toBe("1.23e+6");
222
+ expect(getByTestId("matrix-cell-1-0").textContent).toBe("0.00e+0");
223
+ expect(getByTestId("matrix-cell-1-1").textContent).toBe("-4.20e-2");
224
+ });
225
+
226
+ it("drag adjusts cell value", () => {
227
+ const plugin = new MatrixPlugin();
228
+ const setValueMock = vi.fn();
229
+ const props = makeProps({
230
+ value: [
231
+ [0, 0],
232
+ [0, 0],
233
+ ],
234
+ setValue: setValueMock,
235
+ });
236
+ const { getByTestId } = render(plugin.render(props));
237
+ const cell = getByTestId("matrix-cell-0-0");
238
+ const container = getByTestId("marimo-plugin-matrix");
239
+
240
+ // Pointer down on cell (0,0), then move 30px right = 3 steps
241
+ fireEvent.pointerDown(cell, { clientX: 100, pointerId: 1 });
242
+ fireEvent.pointerMove(container, { clientX: 130 });
243
+ fireEvent.pointerUp(container);
244
+
245
+ expect(setValueMock).toHaveBeenCalledWith([
246
+ [3, 0],
247
+ [0, 0],
248
+ ]);
249
+ });
250
+
251
+ it("symmetric mode mirrors value to transpose cell", () => {
252
+ const plugin = new MatrixPlugin();
253
+ const setValueMock = vi.fn();
254
+ const props = makeProps({
255
+ value: [
256
+ [0, 0],
257
+ [0, 0],
258
+ ],
259
+ setValue: setValueMock,
260
+ data: {
261
+ ...makeProps().data,
262
+ symmetric: true,
263
+ },
264
+ });
265
+ const { getByTestId } = render(plugin.render(props));
266
+ const cell = getByTestId("matrix-cell-0-1");
267
+ const container = getByTestId("marimo-plugin-matrix");
268
+
269
+ fireEvent.pointerDown(cell, { clientX: 100, pointerId: 1 });
270
+ fireEvent.pointerMove(container, { clientX: 120 }); // 2 steps
271
+ fireEvent.pointerUp(container);
272
+
273
+ // Cell (0,1) and (1,0) should both be 2
274
+ expect(setValueMock).toHaveBeenCalledWith([
275
+ [0, 2],
276
+ [2, 0],
277
+ ]);
278
+ });
279
+
280
+ it("ArrowUp increments cell value", () => {
281
+ const plugin = new MatrixPlugin();
282
+ const setValueMock = vi.fn();
283
+ const props = makeProps({
284
+ value: [
285
+ [5, 0],
286
+ [0, 0],
287
+ ],
288
+ setValue: setValueMock,
289
+ });
290
+ const { getByTestId } = render(plugin.render(props));
291
+
292
+ fireEvent.keyDown(getByTestId("matrix-cell-0-0"), { key: "ArrowUp" });
293
+
294
+ expect(setValueMock).toHaveBeenCalledWith([
295
+ [6, 0],
296
+ [0, 0],
297
+ ]);
298
+ });
299
+
300
+ it("ArrowDown decrements cell value", () => {
301
+ const plugin = new MatrixPlugin();
302
+ const setValueMock = vi.fn();
303
+ const props = makeProps({
304
+ value: [
305
+ [5, 0],
306
+ [0, 0],
307
+ ],
308
+ setValue: setValueMock,
309
+ });
310
+ const { getByTestId } = render(plugin.render(props));
311
+
312
+ fireEvent.keyDown(getByTestId("matrix-cell-0-0"), { key: "ArrowDown" });
313
+
314
+ expect(setValueMock).toHaveBeenCalledWith([
315
+ [4, 0],
316
+ [0, 0],
317
+ ]);
318
+ });
319
+
320
+ it("disabled cells ignore pointer and keyboard input", () => {
321
+ const plugin = new MatrixPlugin();
322
+ const setValueMock = vi.fn();
323
+ const props = makeProps({
324
+ value: [
325
+ [5, 0],
326
+ [0, 0],
327
+ ],
328
+ setValue: setValueMock,
329
+ data: {
330
+ ...makeProps().data,
331
+ disabled: [
332
+ [true, false],
333
+ [false, false],
334
+ ],
335
+ },
336
+ });
337
+ const { getByTestId } = render(plugin.render(props));
338
+ const cell = getByTestId("matrix-cell-0-0");
339
+ const container = getByTestId("marimo-plugin-matrix");
340
+
341
+ // Keyboard
342
+ fireEvent.keyDown(cell, { key: "ArrowUp" });
343
+ expect(setValueMock).not.toHaveBeenCalled();
344
+
345
+ // Drag
346
+ fireEvent.pointerDown(cell, { clientX: 100, pointerId: 1 });
347
+ fireEvent.pointerMove(container, { clientX: 130 });
348
+ fireEvent.pointerUp(container);
349
+ expect(setValueMock).not.toHaveBeenCalled();
350
+ });
351
+
352
+ it("clamps values to min/max bounds", () => {
353
+ const plugin = new MatrixPlugin();
354
+ const setValueMock = vi.fn();
355
+ const props = makeProps({
356
+ value: [
357
+ [5, 0],
358
+ [0, 0],
359
+ ],
360
+ setValue: setValueMock,
361
+ data: {
362
+ ...makeProps().data,
363
+ minValue: [
364
+ [0, 0],
365
+ [0, 0],
366
+ ],
367
+ maxValue: [
368
+ [6, 10],
369
+ [10, 10],
370
+ ],
371
+ },
372
+ });
373
+ const { getByTestId } = render(plugin.render(props));
374
+ const cell = getByTestId("matrix-cell-0-0");
375
+ const container = getByTestId("marimo-plugin-matrix");
376
+
377
+ // Try to drag far right (would be +10 without clamping)
378
+ fireEvent.pointerDown(cell, { clientX: 100, pointerId: 1 });
379
+ fireEvent.pointerMove(container, { clientX: 200 });
380
+ fireEvent.pointerUp(container);
381
+
382
+ // Should be clamped to max of 6
383
+ expect(setValueMock).toHaveBeenCalledWith([
384
+ [6, 0],
385
+ [0, 0],
386
+ ]);
387
+ });
388
+
389
+ it("sets aria attributes on cells", () => {
390
+ const plugin = new MatrixPlugin();
391
+ const props = makeProps({
392
+ data: {
393
+ ...makeProps().data,
394
+ rowLabels: ["x", "y"],
395
+ columnLabels: ["a", "b"],
396
+ minValue: [
397
+ [0, 0],
398
+ [0, 0],
399
+ ],
400
+ maxValue: [
401
+ [10, 10],
402
+ [10, 10],
403
+ ],
404
+ },
405
+ });
406
+ const { getByTestId } = render(plugin.render(props));
407
+ const cell = getByTestId("matrix-cell-0-1");
408
+
409
+ expect(cell.getAttribute("aria-label")).toBe("x, b");
410
+ expect(cell.getAttribute("aria-valuenow")).toBe("2");
411
+ expect(cell.getAttribute("aria-valuemin")).toBe("0");
412
+ expect(cell.getAttribute("aria-valuemax")).toBe("10");
413
+ expect(cell.getAttribute("tabindex")).toBe("0");
414
+ });
415
+ });
@@ -397,11 +397,10 @@ export async function handleWidgetMessage(
397
397
  return;
398
398
  }
399
399
 
400
- case "close": {
400
+ case "close":
401
401
  BINDING_MANAGER.destroy(modelId);
402
402
  modelManager.delete(modelId); // aborts the model's signal, clearing listeners
403
403
  return;
404
- }
405
404
 
406
405
  case "update": {
407
406
  const { state, buffer_paths = [] } = msg;