@marimo-team/islands 0.25.3-dev103 → 0.25.3-dev104

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.
@@ -135,7 +135,7 @@ function F(e) {
135
135
  }
136
136
  //#endregion
137
137
  //#region src/components/editor/boundary/ErrorBoundary.tsx
138
- var I = (0, T.lazy)(() => import("./feedback-button-9Gkf9CUC.js").then((e) => ({ default: e.FeedbackModal }))), L = (e) => {
138
+ var I = (0, T.lazy)(() => import("./feedback-button-CfEYDdvF.js").then((e) => ({ default: e.FeedbackModal }))), L = (e) => {
139
139
  let t = (0, w.c)(2), n;
140
140
  return t[0] === e.children ? n = t[1] : (n = /* @__PURE__ */ (0, j.jsx)(O, {
141
141
  FallbackComponent: R,
@@ -18,7 +18,7 @@ function x() {
18
18
  return e.warn("Failed to get version from mount config"), null;
19
19
  }
20
20
  }
21
- var S = x() || "0.25.3-dev103", C = u(S), w = u(!0);
21
+ var S = x() || "0.25.3-dev104", C = u(S), w = u(!0);
22
22
  u(null);
23
23
  //#endregion
24
24
  //#region src/core/wasm/state.ts
@@ -12,7 +12,7 @@ import { t as lt } from "./check-P35dc2dH.js";
12
12
  import { A as ut, C as dt, M as ft, O as pt, S as mt, _ as ht, b as gt, c as _t, d as vt, g as yt, h as bt, i as xt, l as St, m as Ct, n as wt, p as Tt, s as Et, t as Dt, u as Ot, v as kt, y as At } from "./select-B0LRDH0g.js";
13
13
  import { $ as jt, $t as Mt, A as Nt, At as Pt, B as Ft, Bt as It, Ct as Lt, D as Rt, Dt as zt, E as Bt, Et as Vt, F as Ht, Ft as Ut, G as Wt, Gt, H as Kt, Ht as qt, I as Jt, It as Yt, J as Xt, Jt as Zt, Kt as Qt, L as $t, Lt as en, M as tn, Mt as nn, N as rn, Nt as an, O as on, Ot as sn, P as cn, Pt as ln, Q as un, Qt as dn, R as fn, Rt as pn, S as mn, St as hn, T as gn, Tt as _n, U as vn, Ut as yn, V as bn, Vt as xn, W as Sn, Wt as Cn, X as wn, Xt as Tn, Y as En, Yt as Dn, Z as On, Zt as kn, _ as An, _n as jn, _t as Mn, a as Nn, an as Pn, at as Fn, b as In, bt as Ln, cn as Rn, ct as zn, d as Bn, dn as Vn, en as Hn, et as Un, f as Wn, fn as Gn, ft as Kn, gn as qn, gt as Jn, h as Yn, hn as Xn, ht as Zn, i as Qn, in as $n, it as er, j as tr, jt as nr, k as rr, kt as ir, l as ar, ln as or, lt as sr, mn as cr, mt as lr, n as ur, nn as dr, nt as fr, o as pr, ot as mr, pn as hr, pt as gr, qt as _r, rn as vr, rt as yr, s as br, sn as xr, st as Sr, t as Cr, tt as wr, u as Tr, ut as Er, vn as Dr, vt as Or, w as kr, wt as Ar, x as jr, xt as Mr, y as Nr, yn as Pr, yt as Fr, z as Ir, zt as Lr } from "./dropdown-menu-B_7xD2X4.js";
14
14
  import { a as Rr, c as zr, d as Br, i as Vr, l as Hr, n as Ur, o as Wr, r as Gr, t as Kr, v as qr, x as Jr, y as Yr } from "./toDate-DKH0_kMo.js";
15
- import { n as Xr, t as Zr } from "./ErrorBoundary-DvJIREfH.js";
15
+ import { n as Xr, t as Zr } from "./ErrorBoundary-BSX5FnuK.js";
16
16
  import { t as Qr } from "./clsx-CoeGtWvn.js";
17
17
  import { t as $r } from "./react-dom-DUmH0GXv.js";
18
18
  import { t as ei } from "./jsx-runtime-3BD1kT0s.js";
@@ -2,7 +2,7 @@ import { a as e } from "./rolldown-runtime-Cb76FRpK.js";
2
2
  import { t } from "./react-sHtZdjeP.js";
3
3
  import { t as n } from "./react-dom-DUmH0GXv.js";
4
4
  import { c as r, l as i, u as a } from "./data-grid-types-0PJ-9N5n.js";
5
- import { a as o, i as s, n as c, r as l } from "./glide-data-editor-D2F4hynZ.js";
5
+ import { a as o, i as s, n as c, r as l } from "./glide-data-editor-CuC6Qmmd.js";
6
6
  //#region ../node_modules/.pnpm/@glideapps+glide-data-grid@6.0.4-alpha24_lodash@4.17.21_marked@15.0.12_react-dom@19.3.0_27cffe7f1bc30287d59ac27db3240c5b/node_modules/@glideapps/glide-data-grid/dist/esm/internal/data-grid-overlay-editor/data-grid-overlay-editor-style.js
7
7
  var u = /* @__PURE__ */ e(t(), 1), d = /* @__PURE__ */ e(n(), 1), f = /*#__PURE__*/ o("div")({
8
8
  name: "DataGridOverlayEditorStyle",
@@ -5,12 +5,12 @@ import { t as i } from "./compiler-runtime-CldRdSsv.js";
5
5
  import { t as a } from "./copy-icon-CPoxUG7G.js";
6
6
  import { Dt as o, G as s, It as c, K as l, Rt as u, V as d, dn as f, h as p, i as m, n as h } from "./error-entries-m7wjcSR-.js";
7
7
  import { b as g } from "./select-B0LRDH0g.js";
8
- import { n as ee, t as te } from "./globals-BBV0iOn6.js";
8
+ import { n as ee, t as te } from "./globals-D7jFtSFG.js";
9
9
  import { t as _ } from "./jsx-runtime-3BD1kT0s.js";
10
10
  import { C as v, _ as ne, i as y } from "./useTheme-CfeaVw5D.js";
11
11
  import { a as re, i as ie, n as ae, r as oe } from "./dialog-C1PcpCqX.js";
12
12
  import { t as se } from "./useAsyncData-D0mLJstx.js";
13
- import { t as ce } from "./code-visibility-CfeBTR2t.js";
13
+ import { t as ce } from "./code-visibility-azRFwO8n.js";
14
14
  //#region src/components/ui/skeleton.tsx
15
15
  var b = /* @__PURE__ */ e(r(), 1), x = i(), S = _(), le = (e) => {
16
16
  let n = (0, x.c)(8), r, i;
@@ -6,7 +6,7 @@ import { i as s, n as c, r as l } from "./copy-icon-CPoxUG7G.js";
6
6
  import { C as u, c as d, i as f, n as p, s as m, t as h, x as g } from "./select-B0LRDH0g.js";
7
7
  import { d as _, h as v, i as y, l as b, n as x, o as S, s as C, t as w, u as T } from "./dropdown-menu-B_7xD2X4.js";
8
8
  import { c as E, d as D, l as O, n as k, o as A, s as ee, t as j, u as te } from "./glide-utils-Bt8_pBa2.js";
9
- import { n as ne, t as re } from "./ErrorBoundary-DvJIREfH.js";
9
+ import { n as ne, t as re } from "./ErrorBoundary-BSX5FnuK.js";
10
10
  import { t as ie } from "./react-dom-DUmH0GXv.js";
11
11
  import { t as M } from "./jsx-runtime-3BD1kT0s.js";
12
12
  import { t as N } from "./label-BRnUC0NX.js";
@@ -7062,7 +7062,7 @@ function rs(e, t, n, r, i) {
7062
7062
  }
7063
7063
  //#endregion
7064
7064
  //#region ../node_modules/.pnpm/@glideapps+glide-data-grid@6.0.4-alpha24_lodash@4.17.21_marked@15.0.12_react-dom@19.3.0_27cffe7f1bc30287d59ac27db3240c5b/node_modules/@glideapps/glide-data-grid/dist/esm/data-editor/data-editor.js
7065
- var is = /* @__PURE__ */ e(_r(), 1), as = /* @__PURE__ */ e(xr(), 1), os = K.lazy(async () => await import("./data-grid-overlay-editor-DEC7lFIy.js")), ss = 0;
7065
+ var is = /* @__PURE__ */ e(_r(), 1), as = /* @__PURE__ */ e(xr(), 1), os = K.lazy(async () => await import("./data-grid-overlay-editor-BvWRCxAv.js")), ss = 0;
7066
7066
  function cs(e) {
7067
7067
  return (0, is.default)((0, as.default)((0, as.default)(e).filter((e) => e.span !== void 0).map((e) => (0, Za.default)((e.span?.[0] ?? 0) + 1, (e.span?.[1] ?? 0) + 1))));
7068
7068
  }
@@ -9293,7 +9293,7 @@ function Vs(e, t, n, r, i) {
9293
9293
  }
9294
9294
  //#endregion
9295
9295
  //#region ../node_modules/.pnpm/@glideapps+glide-data-grid@6.0.4-alpha24_lodash@4.17.21_marked@15.0.12_react-dom@19.3.0_27cffe7f1bc30287d59ac27db3240c5b/node_modules/@glideapps/glide-data-grid/dist/esm/cells/number-cell.js
9296
- var Hs = K.lazy(async () => await import("./number-overlay-editor-B3XQt_PV.js")), Us = {
9296
+ var Hs = K.lazy(async () => await import("./number-overlay-editor-CCzf5gxX.js")), Us = {
9297
9297
  getAccessibilityString: (e) => e.data?.toString() ?? "",
9298
9298
  kind: W.Number,
9299
9299
  needsHover: (e) => e.hoverEffect === !0,
@@ -1,6 +1,6 @@
1
1
  import { h as e } from "./tooltip-CKO1pHJo.js";
2
2
  import { _ as t } from "./useTheme-CfeaVw5D.js";
3
- import { r as n } from "./code-visibility-CfeBTR2t.js";
3
+ import { r as n } from "./code-visibility-azRFwO8n.js";
4
4
  var r = e("triangle-alert", [
5
5
  ["path", {
6
6
  d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3",
package/dist/main.js CHANGED
@@ -7,7 +7,7 @@ import { C as w, F as T, G as E, I as D, J as O, K as k, L as A, P as j, R as M,
7
7
  import { a as oe, d as se, f as ce, h as V, i as le, n as ue, o as de, r as fe, t as pe } from "./tooltip-CKO1pHJo.js";
8
8
  import { $t as me, At as he, B as ge, Bt as _e, D as ve, Dt as ye, E as be, Et as xe, Ft as Se, Gt as Ce, J as we, Jt as Te, Kt as Ee, M as De, Mt as Oe, N as ke, O as Ae, Ot as je, Q as Me, R as Ne, S as Pe, Ut as Fe, V as Ie, Vt as Le, W as Re, Y as ze, Yt as Be, Z as Ve, _ as He, b as Ue, bt as We, cn as Ge, dt as Ke, en as qe, f as Je, fn as Ye, ft as Xe, g as Ze, gn as Qe, gt as $e, h as et, hn as tt, ht as nt, in as rt, j as it, jt as at, k as ot, kt as st, ln as ct, m as lt, nn as ut, nt as dt, o as ft, p as pt, pn as mt, q as ht, qt as gt, rt as _t, s as vt, sn as yt, st as bt, tn as xt, u as St, un as Ct, ut as wt, v as Tt, wt as Et, y as Dt, z as Ot, zt as kt } from "./error-entries-m7wjcSR-.js";
9
9
  import { D as At, E as jt, O as Mt, S as Nt, T as Pt, _ as Ft, a as It, c as Lt, d as Rt, f as zt, g as Bt, h as Vt, i as Ht, l as Ut, m as Wt, n as Gt, o as Kt, p as qt, r as Jt, s as Yt, t as Xt, u as Zt, v as Qt, w as $t, x as en, y as tn } from "./spec-CcT0slVc.js";
10
- import { $ as nn, $t as rn, A as an, At as on, B as sn, Bt as cn, C as ln, Ct as un, D as dn, Dt as fn, E as pn, Et as mn, F as hn, Ft as gn, G as _n, Gt as vn, H as yn, Ht as bn, I as xn, It as Sn, J as Cn, Jt as wn, K as Tn, Kt as En, L as Dn, Lt as On, M as kn, Mt as An, N as jn, Nt as Mn, O as Nn, Ot as Pn, P as Fn, Pt as In, Qt as Ln, R as Rn, Rt as zn, S as Bn, St as Vn, T as Hn, Tt as Un, U as Wn, Ut as Gn, V as Kn, Vt as qn, W as Jn, Wt as Yn, Xt as Xn, Y as Zn, Yt as Qn, Zt as $n, _ as er, _t as tr, at as nr, b as rr, c as ir, cn as ar, ct as or, d as sr, dn as cr, dt as lr, en as ur, et as dr, f as fr, fn as pr, ft as mr, g as hr, gt as gr, h as _r, ht as vr, in as yr, it as br, j as xr, jt as Sr, k as Cr, kt as wr, ln as Tr, lt as Er, m as Dr, mn as Or, mt as kr, nn as Ar, nt as jr, on as Mr, ot as Nr, p as Pr, pn as Fr, pt as Ir, q as Lr, qt as Rr, r as zr, rn as Br, rt as Vr, sn as Hr, st as Ur, t as Wr, tn as Gr, tt as Kr, u as qr, un as Jr, ut as Yr, v as Xr, vt as Zr, w as Qr, wt as $r, x as ei, xt as ti, y as ni, z as ri, zt as ii } from "./common-BRSsfrNv.js";
10
+ import { $ as nn, $t as rn, A as an, At as on, B as sn, Bt as cn, C as ln, Ct as un, D as dn, Dt as fn, E as pn, Et as mn, F as hn, Ft as gn, G as _n, Gt as vn, H as yn, Ht as bn, I as xn, It as Sn, J as Cn, Jt as wn, K as Tn, Kt as En, L as Dn, Lt as On, M as kn, Mt as An, N as jn, Nt as Mn, O as Nn, Ot as Pn, P as Fn, Pt as In, Qt as Ln, R as Rn, Rt as zn, S as Bn, St as Vn, T as Hn, Tt as Un, U as Wn, Ut as Gn, V as Kn, Vt as qn, W as Jn, Wt as Yn, Xt as Xn, Y as Zn, Yt as Qn, Zt as $n, _ as er, _t as tr, at as nr, b as rr, c as ir, cn as ar, ct as or, d as sr, dn as cr, dt as lr, en as ur, et as dr, f as fr, fn as pr, ft as mr, g as hr, gt as gr, h as _r, ht as vr, in as yr, it as br, j as xr, jt as Sr, k as Cr, kt as wr, ln as Tr, lt as Er, m as Dr, mn as Or, mt as kr, nn as Ar, nt as jr, on as Mr, ot as Nr, p as Pr, pn as Fr, pt as Ir, q as Lr, qt as Rr, r as zr, rn as Br, rt as Vr, sn as Hr, st as Ur, t as Wr, tn as Gr, tt as Kr, u as qr, un as Jr, ut as Yr, v as Xr, vt as Zr, w as Qr, wt as $r, x as ei, xt as ti, y as ni, z as ri, zt as ii } from "./common-D7dvNADC.js";
11
11
  import { c as ai, i as oi, n as si, o as ci, t as li } from "./useLifecycle-Dum8WSXq.js";
12
12
  import { t as ui } from "./check-P35dc2dH.js";
13
13
  import { A as di, C as fi, D as pi, E as mi, M as hi, O as gi, S as _i, T as vi, a as yi, b as bi, c as xi, i as Si, k as Ci, l as wi, n as Ti, o as Ei, r as Di, s as Oi, t as ki, w as Ai, x as ji } from "./select-B0LRDH0g.js";
@@ -15,8 +15,8 @@ import { At as Mi, C as Ni, Dt as Pi, J as Fi, K as Ii, R as Li, S as Ri, U as z
15
15
  import { C as ua, _ as da, a as fa, b as pa, c as ma, f as ha, h as ga, m as _a, p as va, s as ya, u as ba, x as xa, y as Sa } from "./toDate-DKH0_kMo.js";
16
16
  import { a as Ca, c as wa, d as Ta, f as Ea, i as Da, l as Oa, n as ka, o as Aa, r as ja, s as Ma, t as Na } from "./process-output-DIMk7xHI.js";
17
17
  import { a as Pa, c as Fa, d as Ia, i as La, l as Ra, r as za, u as Ba } from "./glide-utils-Bt8_pBa2.js";
18
- import { n as Va, t as Ha } from "./ErrorBoundary-DvJIREfH.js";
19
- import { n as Ua, t as Wa } from "./globals-BBV0iOn6.js";
18
+ import { n as Va, t as Ha } from "./ErrorBoundary-BSX5FnuK.js";
19
+ import { n as Ua, t as Wa } from "./globals-D7jFtSFG.js";
20
20
  import { t as Ga } from "./react-dom-DUmH0GXv.js";
21
21
  import { t as Ka } from "./jsx-runtime-3BD1kT0s.js";
22
22
  import { B as qa, C as H, Ct as Ja, D as Ya, E as Xa, F as U, G as Za, H as Qa, I as $a, L as eo, M as W, N as to, O as no, P as ro, Q as io, R as ao, S as G, St as oo, Tt as so, U as co, W as lo, Y as uo, Z as fo, _ as po, _t as mo, a as ho, b as go, bt as _o, c as vo, d as yo, f as bo, g as xo, h as So, ht as Co, i as wo, j as K, k as To, l as Eo, m as Do, n as Oo, o as ko, p as Ao, r as jo, s as Mo, u as No, ut as Po, v as Fo, vt as Io, w as Lo, wt as Ro, x as zo, xt as Bo, y as Vo, yt as Ho } from "./use-restore-focus-DBCITLeF.js";
@@ -39,7 +39,7 @@ import { t as Ms } from "./useAsyncData-D0mLJstx.js";
39
39
  import { i as Ns, n as Ps, r as Fs } from "./formats-V8ifA0Bk.js";
40
40
  import { D as Is, O as Ls } from "./vega-loader.browser-B9Pz0EGP.js";
41
41
  import { a as Rs, n as zs } from "./loader-Bm8cy3e8.js";
42
- import { c as Bs, d as Vs, i as Hs, l as Us, o as Ws, s as Gs, t as Ks, u as qs } from "./code-visibility-CfeBTR2t.js";
42
+ import { c as Bs, d as Vs, i as Hs, l as Us, o as Ws, s as Gs, t as Ks, u as qs } from "./code-visibility-azRFwO8n.js";
43
43
  import { t as Js } from "./useDeepCompareMemoize-BpPWi3G3.js";
44
44
  import { t as Ys } from "./esm-D2gTsXEV.js";
45
45
  //#region ../node_modules/.pnpm/iconify-icon@2.3.0/node_modules/iconify-icon/dist/iconify-icon.mjs
@@ -10578,7 +10578,7 @@ function VC(e, t) {
10578
10578
  }
10579
10579
  //#endregion
10580
10580
  //#region src/plugins/impl/DataEditorPlugin.tsx
10581
- var HC = J.lazy(() => import("./glide-data-editor-D2F4hynZ.js").then((e) => e.t)), UC = wS("marimo-data-editor", { cssStyles: [qS] }).withData(W({
10581
+ var HC = J.lazy(() => import("./glide-data-editor-CuC6Qmmd.js").then((e) => e.t)), UC = wS("marimo-data-editor", { cssStyles: [qS] }).withData(W({
10582
10582
  initialValue: W({ edits: G(W({
10583
10583
  rowIdx: K(),
10584
10584
  columnId: U(),
@@ -30305,7 +30305,7 @@ function VR(e) {
30305
30305
  }
30306
30306
  //#endregion
30307
30307
  //#region src/components/editor/renderers/slides-layout/slides-layout.tsx
30308
- var HR = J.lazy(() => import("./reveal-component-DEasliPP.js")), UR = (e) => {
30308
+ var HR = J.lazy(() => import("./reveal-component-ZUn41jSB.js")), UR = (e) => {
30309
30309
  let t = (0, X.c)(33), { layout: n, setLayout: r, cells: i, mode: a } = e, o = ns(da), s = a === "read" || o, c = ns(He) > 1, [l, u] = (0, J.useState)(null), d;
30310
30310
  t[0] !== i || t[1] !== n ? (d = zP(i, n), t[0] = i, t[1] = n, t[2] = d) : d = t[2];
30311
30311
  let { slideCells: f, skippedIds: p, noOutputIds: m, slideTypes: h, startCellIndex: g } = d, _;
@@ -1,6 +1,6 @@
1
1
  import { a as e } from "./rolldown-runtime-Cb76FRpK.js";
2
2
  import { t } from "./react-sHtZdjeP.js";
3
- import { a as n } from "./glide-data-editor-D2F4hynZ.js";
3
+ import { a as n } from "./glide-data-editor-CuC6Qmmd.js";
4
4
  //#region ../node_modules/.pnpm/@glideapps+glide-data-grid@6.0.4-alpha24_lodash@4.17.21_marked@15.0.12_react-dom@19.3.0_27cffe7f1bc30287d59ac27db3240c5b/node_modules/@glideapps/glide-data-grid/dist/esm/internal/data-grid-overlay-editor/private/number-overlay-editor-style.js
5
5
  var r = /* @__PURE__ */ e(t()), i = /*#__PURE__*/ n("div")({
6
6
  name: "NumberOverlayEditorStyle",
@@ -4,14 +4,14 @@ import { t as o } from "./react-sHtZdjeP.js";
4
4
  import { t as s } from "./compiler-runtime-CldRdSsv.js";
5
5
  import { h as c, t as l } from "./tooltip-CKO1pHJo.js";
6
6
  import { O as u } from "./error-entries-m7wjcSR-.js";
7
- import { Q as d, X as f, Z as p, an as m, cn as h, i as g, in as _, l as v, n as y, r as b, t as x, u as S } from "./common-BRSsfrNv.js";
7
+ import { Q as d, X as f, Z as p, an as m, cn as h, i as g, in as _, l as v, n as y, r as b, t as x, u as S } from "./common-D7dvNADC.js";
8
8
  import { M as C } from "./select-B0LRDH0g.js";
9
9
  import { q as ee } from "./dropdown-menu-B_7xD2X4.js";
10
10
  import { _ as w } from "./toDate-DKH0_kMo.js";
11
11
  import { t as T } from "./jsx-runtime-3BD1kT0s.js";
12
12
  import { q as E } from "./use-restore-focus-DBCITLeF.js";
13
13
  import { A as D, C as O, b as k } from "./useTheme-CfeaVw5D.js";
14
- import { a as A, n as te, t as ne } from "./code-visibility-CfeBTR2t.js";
14
+ import { a as A, n as te, t as ne } from "./code-visibility-azRFwO8n.js";
15
15
  /* empty css */
16
16
  var j = c("sticky-note", [["path", {
17
17
  d: "M21 9a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 15 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2z",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.25.3-dev103",
3
+ "version": "0.25.3-dev104",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -0,0 +1,386 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { act, render, renderHook } from "@testing-library/react";
4
+ import { atom, createStore, Provider } from "jotai";
5
+ import type { PropsWithChildren } from "react";
6
+ import { afterEach, describe, expect, it, vi } from "vitest";
7
+ import { Deferred } from "@/utils/Deferred";
8
+ import type { Document } from "../../host/resources/document";
9
+ import {
10
+ createDocumentModel,
11
+ DocumentProvider,
12
+ useCellCode,
13
+ useCellConfig,
14
+ useCellName,
15
+ useCellOrder,
16
+ useDocumentEdit,
17
+ useDocumentSync,
18
+ type DocumentHost,
19
+ } from "..";
20
+ import { cell, CONFIG, document, withCells } from "./fixtures";
21
+
22
+ type HostEdit = DocumentHost["edit"];
23
+ const lifetimes: AbortController[] = [];
24
+
25
+ afterEach(() => {
26
+ act(() => lifetimes.splice(0).forEach((controller) => controller.abort()));
27
+ });
28
+
29
+ function setup(
30
+ initial: Document | null = document(),
31
+ edit: HostEdit = () => new Promise(() => {}),
32
+ ) {
33
+ const store = createStore();
34
+ const source = atom(initial);
35
+ const detached = vi.fn();
36
+ source.onMount = () => detached;
37
+ const controller = new AbortController();
38
+ lifetimes.push(controller);
39
+ const model = createDocumentModel({
40
+ store,
41
+ notebook: { documentAtom: source, edit },
42
+ signal: controller.signal,
43
+ });
44
+ const wrapper = ({ children }: PropsWithChildren) => (
45
+ <Provider store={store}>
46
+ <DocumentProvider model={model}>{children}</DocumentProvider>
47
+ </Provider>
48
+ );
49
+ return { store, source, model, controller, detached, wrapper };
50
+ }
51
+
52
+ describe("document hooks", () => {
53
+ it("flushes edits through the model and aborts work when the session ends", async () => {
54
+ const accepted = withCells(document(), { c1: cell("c1", "local") });
55
+ const edit = vi
56
+ .fn<HostEdit>()
57
+ .mockResolvedValueOnce({
58
+ ok: true,
59
+ status: 200,
60
+ data: { document: accepted.revision, cells: {} },
61
+ })
62
+ .mockImplementation(
63
+ () => new Deferred<Awaited<ReturnType<HostEdit>>>().promise,
64
+ );
65
+ const { store, source, wrapper, controller } = setup(document(), edit);
66
+ const { result, unmount } = renderHook(
67
+ () => ({
68
+ code: useCellCode("c1"),
69
+ edit: useDocumentEdit(),
70
+ sync: useDocumentSync(),
71
+ }),
72
+ { wrapper, reactStrictMode: true },
73
+ );
74
+ act(() => result.current.edit((tx) => tx.setCode("c1", "local")));
75
+ expect(result.current.sync.status).toEqual({ status: "pending" });
76
+ const { flush, retry } = result.current.sync;
77
+ await act(async () => {
78
+ const flushed = flush();
79
+ // Allow the response to arrive before publishing its confirmation.
80
+ await Promise.resolve();
81
+ store.set(source, accepted);
82
+ await expect(flushed).resolves.toEqual(accepted.revision);
83
+ });
84
+ expect(result.current.sync).toEqual({
85
+ status: { status: "idle" },
86
+ flush,
87
+ retry,
88
+ });
89
+ expect(result.current.code).toBe("local");
90
+ act(() => result.current.edit((tx) => tx.setCode("c1", "next")));
91
+ await act(async () => {
92
+ const rejected = (async () => {
93
+ await expect(flush()).rejects.toThrow("session ended");
94
+ })();
95
+ await Promise.resolve();
96
+ unmount();
97
+ controller.abort(new Error("session ended"));
98
+ await rejected;
99
+ });
100
+ expect(edit).toHaveBeenCalledTimes(2);
101
+ expect(edit.mock.calls[1][2].signal.aborted).toBe(true);
102
+ });
103
+
104
+ it("publishes compound edits atomically without changing host records", () => {
105
+ const initial = document();
106
+ const { store, source, wrapper } = setup(initial);
107
+ const seen = vi.fn();
108
+ const { result } = renderHook(
109
+ () => {
110
+ const value = {
111
+ code: useCellCode("c1"),
112
+ name: useCellName("c1"),
113
+ config: useCellConfig("c1"),
114
+ order: useCellOrder(),
115
+ };
116
+ seen(value);
117
+ return { ...value, edit: useDocumentEdit() };
118
+ },
119
+ { wrapper },
120
+ );
121
+ const edit = result.current.edit;
122
+ seen.mockClear();
123
+
124
+ act(() =>
125
+ edit((tx) => {
126
+ tx.setCode("c1", "edited");
127
+ tx.setName("c1", "renamed");
128
+ tx.updateConfig("c1", { hide_code: true });
129
+ tx.setOrder(["c2", "c1"]);
130
+ }),
131
+ );
132
+
133
+ const expected = {
134
+ code: "edited",
135
+ name: "renamed",
136
+ config: { ...CONFIG, hide_code: true },
137
+ order: ["c2", "c1"],
138
+ };
139
+ expect(result.current).toEqual({ ...expected, edit });
140
+ expect(seen.mock.calls.length).toBeGreaterThan(0);
141
+ for (const [value] of seen.mock.calls) {
142
+ expect(value).toEqual(expected);
143
+ }
144
+ expect(store.get(source)).toBe(initial);
145
+
146
+ expect(() =>
147
+ act(() =>
148
+ edit((tx) => {
149
+ tx.setCode("c1", "rolled back");
150
+ tx.deleteCell("missing");
151
+ }),
152
+ ),
153
+ ).toThrow("does not exist");
154
+ expect(result.current).toEqual({ ...expected, edit });
155
+ });
156
+
157
+ it("subscribes to individual fields and keeps the edit function stable", () => {
158
+ const { store, source, wrapper } = setup();
159
+ const onCode = vi.fn();
160
+ const onName = vi.fn();
161
+ const onConfig = vi.fn();
162
+ const onOrder = vi.fn();
163
+ const onEdit = vi.fn();
164
+ function Code() {
165
+ onCode(useCellCode("c1"));
166
+ return null;
167
+ }
168
+ function Name() {
169
+ onName(useCellName("c1"));
170
+ return null;
171
+ }
172
+ function Config() {
173
+ onConfig(useCellConfig("c1"));
174
+ return null;
175
+ }
176
+ function Order() {
177
+ onOrder(useCellOrder());
178
+ return null;
179
+ }
180
+ function Edit() {
181
+ onEdit(useDocumentEdit());
182
+ return null;
183
+ }
184
+ render(
185
+ <>
186
+ <Code />
187
+ <Name />
188
+ <Config />
189
+ <Order />
190
+ <Edit />
191
+ </>,
192
+ { wrapper },
193
+ );
194
+ const edit: ReturnType<typeof useDocumentEdit> = onEdit.mock.calls[0][0];
195
+ const clear = () =>
196
+ [onCode, onName, onConfig, onOrder, onEdit].forEach((fn) =>
197
+ fn.mockClear(),
198
+ );
199
+ clear();
200
+
201
+ act(() => edit((tx) => tx.setCode("c1", "local")));
202
+ expect(onCode).toHaveBeenCalledWith("local");
203
+ expect(onName).not.toHaveBeenCalled();
204
+ expect(onConfig).not.toHaveBeenCalled();
205
+ expect(onOrder).not.toHaveBeenCalled();
206
+ expect(onEdit).not.toHaveBeenCalled();
207
+ clear();
208
+
209
+ act(() => edit((tx) => tx.setCode("c2", "other")));
210
+ expect(
211
+ [onCode, onName, onConfig, onOrder, onEdit].map((fn) => fn.mock.calls),
212
+ ).toEqual([[], [], [], [], []]);
213
+
214
+ act(() =>
215
+ store.set(
216
+ source,
217
+ withCells(document(), {
218
+ c1: cell("c1", "remote", {
219
+ name: "host name",
220
+ config: { ...CONFIG },
221
+ }),
222
+ }),
223
+ ),
224
+ );
225
+ expect(onName).toHaveBeenCalledWith("host name");
226
+ expect(onCode).not.toHaveBeenCalled();
227
+ expect(onConfig).not.toHaveBeenCalled();
228
+ expect(onOrder).not.toHaveBeenCalled();
229
+ expect(onEdit).not.toHaveBeenCalled();
230
+ clear();
231
+
232
+ act(() =>
233
+ edit((tx) => {
234
+ tx.updateConfig("c1", { disabled: true });
235
+ tx.setOrder(["c2", "c1"]);
236
+ }),
237
+ );
238
+ expect(onConfig).toHaveBeenCalledWith({ ...CONFIG, disabled: true });
239
+ expect(onOrder).toHaveBeenCalledWith(["c2", "c1"]);
240
+ expect(onCode).not.toHaveBeenCalled();
241
+ expect(onName).not.toHaveBeenCalled();
242
+ expect(onEdit).not.toHaveBeenCalled();
243
+ });
244
+
245
+ it("switches field subscriptions when the cell id changes", () => {
246
+ const { wrapper } = setup();
247
+ const { result, rerender } = renderHook(
248
+ (id) => ({
249
+ code: useCellCode(id),
250
+ edit: useDocumentEdit(),
251
+ }),
252
+ { wrapper, initialProps: "c1" },
253
+ );
254
+ const edit = result.current.edit;
255
+ rerender("c2");
256
+ expect(result.current).toEqual({ code: "y = 2", edit });
257
+ act(() => edit((tx) => tx.setCode("c1", "old subscription")));
258
+ expect(result.current).toEqual({ code: "y = 2", edit });
259
+ act(() => edit((tx) => tx.setCode("c2", "new subscription")));
260
+ expect(result.current).toEqual({ code: "new subscription", edit });
261
+ });
262
+
263
+ it("distinguishes unavailable documents, hidden source, empty code, and deleted cells", () => {
264
+ const { store, source, wrapper } = setup(null);
265
+ const { result } = renderHook(
266
+ () => ({
267
+ order: useCellOrder(),
268
+ code: useCellCode("c1"),
269
+ name: useCellName("c1"),
270
+ config: useCellConfig("c1"),
271
+ edit: useDocumentEdit(),
272
+ }),
273
+ { wrapper },
274
+ );
275
+ const edit = result.current.edit;
276
+ const absent = {
277
+ code: undefined,
278
+ name: undefined,
279
+ config: undefined,
280
+ edit,
281
+ };
282
+ expect(result.current).toEqual({ ...absent, order: null });
283
+ const hidden = withCells(document(), {
284
+ c1: cell("c1", "", { code: null }),
285
+ });
286
+ act(() => store.set(source, hidden));
287
+ expect(result.current).toEqual({
288
+ code: null,
289
+ name: "_",
290
+ config: CONFIG,
291
+ order: ["c1", "c2"],
292
+ edit,
293
+ });
294
+ act(() => edit((tx) => tx.setCode("c1", "")));
295
+ expect(result.current.code).toBe("");
296
+ act(() => store.set(source, null));
297
+ expect(result.current).toEqual({ ...absent, order: null });
298
+ act(() => store.set(source, hidden));
299
+ expect(result.current.code).toBe("");
300
+ act(() =>
301
+ edit((tx) => {
302
+ tx.deleteCell("c1");
303
+ tx.deleteCell("c2");
304
+ }),
305
+ );
306
+ expect(result.current).toEqual({ ...absent, order: [] });
307
+ });
308
+
309
+ it("preserves drafts across Strict Mode remounts until the session ends", () => {
310
+ const { wrapper, controller, detached } = setup();
311
+ const useProbe = () => ({
312
+ code: useCellCode("c1"),
313
+ edit: useDocumentEdit(),
314
+ });
315
+ const first = renderHook(useProbe, { wrapper, reactStrictMode: true });
316
+ const edit = first.result.current.edit;
317
+ act(() => edit((tx) => tx.setCode("c1", "strict mode edit")));
318
+ expect(first.result.current.code).toBe("strict mode edit");
319
+ first.unmount();
320
+ expect(detached).not.toHaveBeenCalled();
321
+
322
+ const second = renderHook(useProbe, { wrapper, reactStrictMode: true });
323
+ expect(second.result.current.code).toBe("strict mode edit");
324
+ act(() => edit((tx) => tx.setCode("c1", "same session")));
325
+ expect(second.result.current.code).toBe("same session");
326
+ second.unmount();
327
+ controller.abort();
328
+ expect(detached).toHaveBeenCalledOnce();
329
+ const callback = vi.fn();
330
+ edit(callback);
331
+ expect(callback).not.toHaveBeenCalled();
332
+ });
333
+
334
+ it("switches reads and edits to the supplied model and store", () => {
335
+ const first = setup();
336
+ const second = setup(
337
+ withCells(document("n2"), {
338
+ c1: cell("c1", "second source", { notebook_id: "n2" }),
339
+ }),
340
+ );
341
+ const seen = vi.fn();
342
+ function Probe() {
343
+ seen({ code: useCellCode("c1"), edit: useDocumentEdit() });
344
+ return null;
345
+ }
346
+ const tree = ({ store, model }: typeof first) => (
347
+ <Provider store={store}>
348
+ <DocumentProvider model={model}>
349
+ <Probe />
350
+ </DocumentProvider>
351
+ </Provider>
352
+ );
353
+ const { rerender } = render(tree(first));
354
+ const oldEdit: ReturnType<typeof useDocumentEdit> =
355
+ seen.mock.calls.at(-1)![0].edit;
356
+ act(() => oldEdit((tx) => tx.setCode("c1", "first draft")));
357
+ rerender(tree(second));
358
+ act(() => first.controller.abort());
359
+ expect(first.detached).toHaveBeenCalledOnce();
360
+ const latest: {
361
+ code: string | null | undefined;
362
+ edit: ReturnType<typeof useDocumentEdit>;
363
+ } = seen.mock.calls.at(-1)![0];
364
+ expect(latest.code).toBe("second source");
365
+ expect(latest.edit).not.toBe(oldEdit);
366
+ act(() => latest.edit((tx) => tx.setCode("c1", "second draft")));
367
+ expect(seen.mock.calls.at(-1)![0].code).toBe("second draft");
368
+ const stale = vi.fn();
369
+ oldEdit(stale);
370
+ expect(stale).not.toHaveBeenCalled();
371
+ seen.mockClear();
372
+ act(() =>
373
+ first.store.set(
374
+ first.source,
375
+ withCells(document(), {
376
+ c1: cell("c1", "stale host"),
377
+ }),
378
+ ),
379
+ );
380
+ expect(seen).not.toHaveBeenCalled();
381
+ });
382
+
383
+ it("requires a document provider", () => {
384
+ expect(() => renderHook(useDocumentEdit)).toThrow("DocumentProvider");
385
+ });
386
+ });
@@ -0,0 +1,118 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import type { CellId } from "@marimo-team/client/protocol";
4
+ import { useAtomValue, useSetAtom, useStore } from "jotai";
5
+ import { createContext, type ReactNode, useContext, useMemo } from "react";
6
+ import { cellCodeBinding } from "./codemirror";
7
+ import type { EditableCell } from "./draft";
8
+ import type { DocumentModel } from "./model";
9
+ import { selectCellField, selectOrder } from "./select";
10
+
11
+ const DocumentContext = createContext<DocumentModel | null>(null);
12
+
13
+ /**
14
+ * Expose a session-owned model to React. Construct the model with the same store
15
+ * as the enclosing Jotai Provider. The session owner controls its AbortSignal;
16
+ * mounting or unmounting this provider does not change the model's lifetime.
17
+ *
18
+ * ```tsx
19
+ * <Provider store={store}>
20
+ * <DocumentProvider model={model}>
21
+ * <Editor />
22
+ * </DocumentProvider>
23
+ * </Provider>
24
+ * ```
25
+ */
26
+ export function DocumentProvider({
27
+ model,
28
+ children,
29
+ }: {
30
+ model: DocumentModel;
31
+ children: ReactNode;
32
+ }) {
33
+ return (
34
+ <DocumentContext.Provider value={model}>
35
+ {children}
36
+ </DocumentContext.Provider>
37
+ );
38
+ }
39
+
40
+ function useDocumentModel() {
41
+ const model = useContext(DocumentContext);
42
+ if (model === null) {
43
+ throw new Error("Document hooks require a DocumentProvider");
44
+ }
45
+ return model;
46
+ }
47
+
48
+ /** Null while the document is unavailable; an empty array for an empty document. */
49
+ export function useCellOrder() {
50
+ const { documentAtom } = useDocumentModel();
51
+ return useAtomValue(useMemo(() => selectOrder(documentAtom), [documentAtom]));
52
+ }
53
+
54
+ function useCellField<K extends keyof EditableCell>(id: CellId, field: K) {
55
+ const { documentAtom } = useDocumentModel();
56
+ return useAtomValue(
57
+ useMemo(
58
+ () => selectCellField(documentAtom, id, field),
59
+ [documentAtom, id, field],
60
+ ),
61
+ );
62
+ }
63
+
64
+ /** Undefined for an absent cell; null when the host withholds its source. */
65
+ export function useCellCode(id: CellId) {
66
+ return useCellField(id, "code");
67
+ }
68
+
69
+ /** Undefined when the cell or document is unavailable. */
70
+ export function useCellName(id: CellId) {
71
+ return useCellField(id, "name");
72
+ }
73
+
74
+ /** Undefined when the cell or document is unavailable. */
75
+ export function useCellConfig(id: CellId) {
76
+ return useCellField(id, "config");
77
+ }
78
+
79
+ /**
80
+ * A CodeMirror extension that binds the editor's text to a cell's code.
81
+ * Stable until the cell id or document model changes.
82
+ *
83
+ * ```tsx
84
+ * const extension = useCellCodeExtension(cellId);
85
+ * ```
86
+ */
87
+ export function useCellCodeExtension(id: CellId) {
88
+ const model = useDocumentModel();
89
+ const store = useStore();
90
+ return useMemo(
91
+ () => cellCodeBinding({ store, model, cellId: id }),
92
+ [model, store, id],
93
+ );
94
+ }
95
+
96
+ /**
97
+ * A stable edit function. Synchronous callbacks publish atomically or roll back.
98
+ *
99
+ * ```ts
100
+ * const edit = useDocumentEdit();
101
+ * edit((tx) => tx.setCode(cellId, code));
102
+ * ```
103
+ */
104
+ export function useDocumentEdit() {
105
+ return useSetAtom(useDocumentModel().editAtom);
106
+ }
107
+
108
+ /**
109
+ * Sync status and explicit recovery. flush waits for edits, including newer
110
+ * typing, to reach the confirmed source and returns the revision for Run/Save.
111
+ * Rejections retain local edits; retry explicitly resubmits them. Requests
112
+ * with an unknown outcome reuse their original key within the retry window.
113
+ */
114
+ export function useDocumentSync() {
115
+ const model = useDocumentModel();
116
+ const status = useAtomValue(model.syncStatusAtom);
117
+ return { status, flush: model.flush, retry: model.retry };
118
+ }
@@ -1,5 +1,15 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
+ export {
4
+ DocumentProvider,
5
+ useCellCode,
6
+ useCellCodeExtension,
7
+ useCellConfig,
8
+ useCellName,
9
+ useCellOrder,
10
+ useDocumentEdit,
11
+ useDocumentSync,
12
+ } from "./hooks";
3
13
  export { createDocumentModel } from "./model";
4
14
  export type {
5
15
  DocumentEdit,