@marimo-team/islands 0.25.3-dev93 → 0.25.3-dev95

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-BpHnBE3A.js").then((e) => ({ default: e.FeedbackModal }))), L = (e) => {
138
+ var I = (0, T.lazy)(() => import("./feedback-button-Ct9dNjaH.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-dev93", C = u(S), w = u(!0);
21
+ var S = x() || "0.25.3-dev95", 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-jiRCrm1p.js";
15
+ import { n as Xr, t as Zr } from "./ErrorBoundary-Cghv7nv2.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-B3xUjh0M.js";
5
+ import { a as o, i as s, n as c, r as l } from "./glide-data-editor-D30nP9Xm.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-BZiemZfD.js";
7
7
  import { b as g } from "./select-B0LRDH0g.js";
8
- import { n as ee, t as te } from "./globals-DI7gqOyy.js";
8
+ import { n as ee, t as te } from "./globals-B7SyAmCL.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-B_y_dByX.js";
13
+ import { t as ce } from "./code-visibility-CLLrmROz.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-jiRCrm1p.js";
9
+ import { n as ne, t as re } from "./ErrorBoundary-Cghv7nv2.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-CJTJtfj-.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-CWCIs2Mn.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-DQ6KAcGy.js")), Us = {
9296
+ var Hs = K.lazy(async () => await import("./number-overlay-editor-8Y4ehcLw.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-B_y_dByX.js";
3
+ import { r as n } from "./code-visibility-CLLrmROz.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-BZiemZfD.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-CmpSkUjV.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-BUSxx69_.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-BKLVbFnU.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-jiRCrm1p.js";
19
- import { n as Ua, t as Wa } from "./globals-DI7gqOyy.js";
18
+ import { n as Va, t as Ha } from "./ErrorBoundary-Cghv7nv2.js";
19
+ import { n as Ua, t as Wa } from "./globals-B7SyAmCL.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-B_y_dByX.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-CLLrmROz.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-B3xUjh0M.js").then((e) => e.t)), UC = wS("marimo-data-editor", { cssStyles: [qS] }).withData(W({
10581
+ var HC = J.lazy(() => import("./glide-data-editor-D30nP9Xm.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(),
@@ -30291,7 +30291,7 @@ function BR(e) {
30291
30291
  }
30292
30292
  //#endregion
30293
30293
  //#region src/components/editor/renderers/slides-layout/slides-layout.tsx
30294
- var VR = J.lazy(() => import("./reveal-component-DsWQWmYM.js")), HR = (e) => {
30294
+ var VR = J.lazy(() => import("./reveal-component-D8VAiQY5.js")), HR = (e) => {
30295
30295
  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;
30296
30296
  t[0] !== i || t[1] !== n ? (d = RP(i, n), t[0] = i, t[1] = n, t[2] = d) : d = t[2];
30297
30297
  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-B3xUjh0M.js";
3
+ import { a as n } from "./glide-data-editor-D30nP9Xm.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-BZiemZfD.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-CmpSkUjV.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-BUSxx69_.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-B_y_dByX.js";
14
+ import { a as A, n as te, t as ne } from "./code-visibility-CLLrmROz.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-dev93",
3
+ "version": "0.25.3-dev95",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -0,0 +1,116 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { fireEvent, render, screen } from "@testing-library/react";
4
+ import { describe, expect, it } from "vitest";
5
+ import { CreateNewNotebook } from "../home-page";
6
+
7
+ function newNotebookKey(link: HTMLElement): string | null {
8
+ return new URL(
9
+ link.getAttribute("href") ?? "",
10
+ window.location.href,
11
+ ).searchParams.get("file");
12
+ }
13
+
14
+ function activate(link: HTMLElement, type: string, init: MouseEventInit) {
15
+ fireEvent(link, new MouseEvent(type, { bubbles: true, ...init }));
16
+ }
17
+
18
+ describe("CreateNewNotebook", () => {
19
+ it("links to an untitled notebook", () => {
20
+ render(<CreateNewNotebook />);
21
+ const link = screen.getByRole("link", { name: /new notebook/i });
22
+
23
+ expect(newNotebookKey(link)).toMatch(/^__new__/);
24
+ expect(link).toHaveAttribute("target", "_blank");
25
+ });
26
+
27
+ it.each([
28
+ { label: "click", type: "click", init: { button: 0, detail: 1 } },
29
+ // Enter on a focused link fires `click` with detail 0
30
+ { label: "keyboard activation", type: "click", init: { detail: 0 } },
31
+ // ctrl/cmd-click opens a tab and still fires `click`
32
+ { label: "ctrl-click", type: "click", init: { ctrlKey: true } },
33
+ { label: "cmd-click", type: "click", init: { metaKey: true } },
34
+ // middle-click opens a tab without firing `click`
35
+ { label: "middle-click", type: "auxclick", init: { button: 1 } },
36
+ // "Open link in new tab" from the context menu fires no click at all
37
+ { label: "right-click", type: "contextmenu", init: { button: 2 } },
38
+ ])("uses a fresh notebook key on every $label", ({ type, init }) => {
39
+ render(<CreateNewNotebook />);
40
+ const link = screen.getByRole("link", { name: /new notebook/i });
41
+
42
+ const keys = new Set<string | null>([newNotebookKey(link)]);
43
+ for (let i = 0; i < 2; i++) {
44
+ activate(link, type, init);
45
+ keys.add(newNotebookKey(link));
46
+ }
47
+
48
+ expect(keys.size).toBe(3);
49
+ });
50
+
51
+ it("never hands the browser a key that an earlier activation used", () => {
52
+ render(<CreateNewNotebook />);
53
+ const link = screen.getByRole("link", { name: /new notebook/i });
54
+
55
+ // The browser reads `href` after every listener ran. Record it there.
56
+ const followed: Array<string | null> = [];
57
+ const record = () => followed.push(newNotebookKey(link));
58
+ document.addEventListener("click", record);
59
+ document.addEventListener("auxclick", record);
60
+ try {
61
+ activate(link, "contextmenu", { button: 2 });
62
+ activate(link, "click", { button: 0 });
63
+ activate(link, "click", { button: 0 });
64
+ activate(link, "auxclick", { button: 1 });
65
+ activate(link, "contextmenu", { button: 2 });
66
+ activate(link, "click", { button: 0, metaKey: true });
67
+ } finally {
68
+ document.removeEventListener("click", record);
69
+ document.removeEventListener("auxclick", record);
70
+ }
71
+
72
+ expect(followed).toHaveLength(4);
73
+ expect(new Set(followed).size).toBe(4);
74
+ for (const key of followed) {
75
+ expect(key).toMatch(/^__new__s_/);
76
+ }
77
+ });
78
+
79
+ it("puts a fresh key into the drag payload", () => {
80
+ render(<CreateNewNotebook />);
81
+ const link = screen.getByRole("link", { name: /new notebook/i });
82
+ const before = newNotebookKey(link);
83
+
84
+ const payload = new Map<string, string>();
85
+ const dataTransfer = {
86
+ setData: (format: string, data: string) => payload.set(format, data),
87
+ };
88
+ fireEvent.dragStart(link, { dataTransfer });
89
+
90
+ const dragged = new URL(
91
+ payload.get("text/uri-list") ?? "",
92
+ window.location.href,
93
+ ).searchParams.get("file");
94
+ expect(dragged).toMatch(/^__new__s_/);
95
+ expect(dragged).not.toBe(before);
96
+ expect(payload.get("text/plain")).toBe(payload.get("text/uri-list"));
97
+ // The link itself moves to the dragged key, so a later click rotates again
98
+ expect(newNotebookKey(link)).toBe(dragged);
99
+ });
100
+
101
+ it("survives a re-render without reusing a key", () => {
102
+ const { rerender } = render(<CreateNewNotebook />);
103
+ const link = screen.getByRole("link", { name: /new notebook/i });
104
+
105
+ const keys = new Set<string | null>([newNotebookKey(link)]);
106
+ activate(link, "click", { button: 0 });
107
+ keys.add(newNotebookKey(link));
108
+ rerender(<CreateNewNotebook />);
109
+ keys.add(newNotebookKey(link));
110
+ activate(link, "click", { button: 0 });
111
+ keys.add(newNotebookKey(link));
112
+
113
+ // A re-render keeps the current key; only activations rotate it
114
+ expect(keys.size).toBe(3);
115
+ });
116
+ });
@@ -667,15 +667,36 @@ const SessionShutdownButton: React.FC<{ filePath: string }> = ({
667
667
  );
668
668
  };
669
669
 
670
- const CreateNewNotebook: React.FC = () => {
671
- const url = newNotebookURL();
670
+ export const CreateNewNotebook: React.FC = () => {
671
+ // Reusing a `__new__` key reattaches to an old session; rotate it per open
672
+ const [url, setUrl] = useState(newNotebookURL);
673
+ const rotateUrl = () => {
674
+ const next = newNotebookURL();
675
+ setUrl(next);
676
+ return next;
677
+ };
678
+ const rotateDragPayload = (event: React.DragEvent<HTMLAnchorElement>) => {
679
+ // The browser copies the href into the drag payload before this handler
680
+ // runs, so the rotated URL must be written into the payload by hand
681
+ const next = rotateUrl();
682
+ event.dataTransfer.setData("text/uri-list", next);
683
+ event.dataTransfer.setData("text/plain", next);
684
+ };
672
685
  return (
673
686
  <Button
674
687
  asChild={true}
675
688
  size="sm"
676
689
  className="gap-2 rounded-md border-(--brand) bg-(--brand) text-(--brand-foreground) shadow-none hover:bg-(--brand-hover)"
677
690
  >
678
- <a href={url} target="_blank" rel="noreferrer">
691
+ <a
692
+ href={url}
693
+ target="_blank"
694
+ rel="noreferrer"
695
+ onClick={rotateUrl}
696
+ onAuxClick={rotateUrl}
697
+ onContextMenu={rotateUrl}
698
+ onDragStart={rotateDragPayload}
699
+ >
679
700
  <PlusIcon className="h-4 w-4" />
680
701
  New notebook
681
702
  </a>
@@ -0,0 +1,322 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { describe, expect, it } from "vitest";
4
+ import {
5
+ acknowledge,
6
+ createCell,
7
+ deleteCell,
8
+ type Draft,
9
+ type EditableCell,
10
+ editCell,
11
+ EMPTY_DRAFT,
12
+ setOrder,
13
+ toChanges,
14
+ viewDocument,
15
+ } from "../draft";
16
+ import { cell, CONFIG, document, withCells } from "./fixtures";
17
+
18
+ const confirmed = document();
19
+
20
+ function edits(...steps: Parameters<typeof editCell>[3][]): Draft {
21
+ return steps.reduce(
22
+ (draft, edit) => editCell(confirmed, draft, "c1", edit),
23
+ EMPTY_DRAFT,
24
+ );
25
+ }
26
+
27
+ describe("editCell", () => {
28
+ it("lets later fields win and merges config field by field", () => {
29
+ const draft = edits(
30
+ { code: "a", config: { hide_code: true } },
31
+ { name: "n", config: { column: 1, hide_code: undefined } },
32
+ );
33
+ expect(viewDocument(confirmed, draft).cells.get("c1")).toEqual({
34
+ id: "c1",
35
+ code: "a",
36
+ name: "n",
37
+ config: { ...CONFIG, hide_code: true, column: 1 },
38
+ });
39
+ });
40
+
41
+ it("keeps a reversal to the confirmed value as a draft", () => {
42
+ const draft = edits({ code: "local" }, { code: "x = 1" });
43
+ const remote = withCells(confirmed, { c1: cell("c1", "earlier edit") });
44
+ expect(viewDocument(remote, draft).cells.get("c1")?.code).toBe("x = 1");
45
+ });
46
+
47
+ it("accepts empty strings, false and null while ignoring undefined", () => {
48
+ const draft = edits(
49
+ { code: "", config: { column: 1, disabled: true } },
50
+ {
51
+ code: undefined,
52
+ config: { column: null, disabled: false, hide_code: undefined },
53
+ },
54
+ );
55
+ expect(viewDocument(confirmed, draft).cells.get("c1")).toEqual({
56
+ id: "c1",
57
+ code: "",
58
+ name: "_",
59
+ config: CONFIG,
60
+ });
61
+ });
62
+ });
63
+
64
+ describe("createCell and deleteCell", () => {
65
+ it("fills protocol defaults and places the cell in the view", () => {
66
+ const atEnd = createCell(EMPTY_DRAFT, { id: "c3", code: "3" });
67
+ const after = createCell(
68
+ EMPTY_DRAFT,
69
+ { id: "c3", code: "3" },
70
+ { after: "c1" },
71
+ );
72
+ const before = createCell(
73
+ EMPTY_DRAFT,
74
+ { id: "c3", code: "3" },
75
+ { before: "c1" },
76
+ );
77
+ const orphan = createCell(
78
+ EMPTY_DRAFT,
79
+ { id: "c3", code: "3" },
80
+ { after: "gone" },
81
+ );
82
+ expect(viewDocument(confirmed, atEnd).order).toEqual(["c1", "c2", "c3"]);
83
+ expect(viewDocument(confirmed, after).order).toEqual(["c1", "c3", "c2"]);
84
+ expect(viewDocument(confirmed, before).order).toEqual(["c3", "c1", "c2"]);
85
+ expect(viewDocument(confirmed, orphan).order).toEqual(["c1", "c2", "c3"]);
86
+ expect(viewDocument(confirmed, atEnd).cells.get("c3")).toEqual({
87
+ id: "c3",
88
+ code: "3",
89
+ name: "_",
90
+ config: CONFIG,
91
+ });
92
+ });
93
+
94
+ it("forgets a created cell when it is deleted", () => {
95
+ const draft = deleteCell(
96
+ createCell(EMPTY_DRAFT, { id: "c3", code: "" }),
97
+ "c3",
98
+ );
99
+ expect(toChanges(confirmed, draft)).toEqual([]);
100
+ expect(viewDocument(confirmed, draft).order).toEqual(["c1", "c2"]);
101
+ });
102
+ });
103
+
104
+ describe("viewDocument", () => {
105
+ it("preserves withheld source separately from an empty cell", () => {
106
+ const hidden = cell("c1", "", { code: null });
107
+ const empty = cell("c2", "");
108
+ const source = withCells(confirmed, { c1: hidden, c2: empty });
109
+ expect(viewDocument(source, EMPTY_DRAFT)).toEqual({
110
+ order: ["c1", "c2"],
111
+ cells: new Map([
112
+ ["c1", hidden],
113
+ ["c2", empty],
114
+ ]),
115
+ });
116
+ expect(toChanges(source, EMPTY_DRAFT)).toEqual([]);
117
+ });
118
+
119
+ it("follows the host for untouched fields, cells and order", () => {
120
+ const draft = edits({ code: "local", config: { hide_code: true } });
121
+ const remote = withCells(
122
+ confirmed,
123
+ {
124
+ c1: cell("c1", "remote", {
125
+ name: "renamed",
126
+ config: { ...CONFIG, disabled: true },
127
+ version: 2,
128
+ }),
129
+ c2: cell("c2", "remote too"),
130
+ },
131
+ ["c2", "c1"],
132
+ );
133
+ expect(viewDocument(remote, draft)).toEqual({
134
+ order: ["c2", "c1"],
135
+ cells: new Map<string, EditableCell | undefined>([
136
+ [
137
+ "c1",
138
+ {
139
+ id: "c1",
140
+ code: "local",
141
+ name: "renamed",
142
+ config: { ...CONFIG, disabled: true, hide_code: true },
143
+ },
144
+ ],
145
+ ["c2", remote.cells.get("c2")],
146
+ ]),
147
+ });
148
+ });
149
+ });
150
+
151
+ describe("toChanges", () => {
152
+ it.each([{ name: "renamed" }, { config: { hide_code: true } }])(
153
+ "withholds the whole request when an edited cell lacks source: %o",
154
+ (edit) => {
155
+ const hidden = withCells(confirmed, {
156
+ c1: cell("c1", "", { code: null }),
157
+ });
158
+ const created = createCell(EMPTY_DRAFT, { id: "c3", code: "" });
159
+ const draft = editCell(hidden, created, "c1", edit);
160
+ expect(toChanges(hidden, draft)).toBeNull();
161
+ expect(viewDocument(hidden, draft).cells.get("c1")?.code).toBeNull();
162
+ },
163
+ );
164
+
165
+ it.each(["replacement", ""])(
166
+ "uses explicitly supplied source %j when the host withholds it",
167
+ (code) => {
168
+ const hidden = withCells(confirmed, {
169
+ c1: cell("c1", "", { code: null }),
170
+ });
171
+ const draft = editCell(hidden, EMPTY_DRAFT, "c1", {
172
+ name: "renamed",
173
+ code,
174
+ });
175
+ expect(toChanges(hidden, draft)).toEqual([
176
+ {
177
+ kind: "put_cell",
178
+ base_etag: "c1-etag",
179
+ cell: { id: "c1", code, name: "renamed", config: CONFIG },
180
+ },
181
+ ]);
182
+ },
183
+ );
184
+
185
+ it("can delete and reorder cells without their source", () => {
186
+ const hidden = withCells(confirmed, {
187
+ c1: cell("c1", "", { code: null }),
188
+ c2: cell("c2", "", { code: null }),
189
+ });
190
+ expect(toChanges(hidden, deleteCell(EMPTY_DRAFT, "c1"))).toEqual([
191
+ { kind: "delete_cell", id: "c1", base_etag: "c1-etag" },
192
+ ]);
193
+ expect(toChanges(hidden, setOrder(EMPTY_DRAFT, ["c2", "c1"]))).toEqual([
194
+ { kind: "set_order", base_etag: "order-1", order: ["c2", "c1"] },
195
+ ]);
196
+ });
197
+
198
+ it("is empty for an empty draft and omits puts that change nothing", () => {
199
+ expect(toChanges(confirmed, EMPTY_DRAFT)).toEqual([]);
200
+ expect(
201
+ toChanges(confirmed, edits({ code: "local" }, { code: "x = 1" })),
202
+ ).toEqual([]);
203
+ });
204
+
205
+ it("puts the merged cell against the confirmed etag", () => {
206
+ const draft = edits({ code: "local" });
207
+ const remote = withCells(confirmed, {
208
+ c1: cell("c1", "remote", { name: "renamed", etag: "c1-etag-2" }),
209
+ });
210
+ expect(toChanges(remote, draft)).toEqual([
211
+ {
212
+ kind: "put_cell",
213
+ base_etag: "c1-etag-2",
214
+ cell: { id: "c1", code: "local", name: "renamed", config: CONFIG },
215
+ },
216
+ ]);
217
+ });
218
+
219
+ it.each([{ after: "c3" }, { before: "c3" }])(
220
+ "can anchor to a cell created earlier in the request: %o",
221
+ (placement) => {
222
+ let draft = createCell(EMPTY_DRAFT, { id: "c3", code: "3" });
223
+ draft = createCell(draft, { id: "c4", code: "4" }, placement);
224
+ expect(toChanges(confirmed, draft)).toEqual([
225
+ {
226
+ kind: "put_cell",
227
+ base_etag: null,
228
+ cell: { id: "c3", code: "3", name: "_", config: CONFIG },
229
+ },
230
+ {
231
+ kind: "put_cell",
232
+ base_etag: null,
233
+ cell: { id: "c4", code: "4", name: "_", config: CONFIG },
234
+ ...placement,
235
+ },
236
+ ]);
237
+ },
238
+ );
239
+
240
+ it.each([{ after: "c2" }, { before: "c2" }])(
241
+ "omits an anchor deleted earlier in the request: %o",
242
+ (placement) => {
243
+ let draft = deleteCell(EMPTY_DRAFT, "c2");
244
+ draft = createCell(draft, { id: "c3", code: "3" }, placement);
245
+ expect(toChanges(confirmed, draft)).toEqual([
246
+ { kind: "delete_cell", id: "c2", base_etag: "c2-etag" },
247
+ {
248
+ kind: "put_cell",
249
+ base_etag: null,
250
+ cell: { id: "c3", code: "3", name: "_", config: CONFIG },
251
+ },
252
+ ]);
253
+ },
254
+ );
255
+
256
+ it("creates, deletes, then sets the reconciled order", () => {
257
+ let draft = setOrder(EMPTY_DRAFT, ["c2", "c1"]);
258
+ draft = createCell(draft, { id: "c3", code: "3" }, { after: "c2" });
259
+ draft = deleteCell(draft, "c1");
260
+ expect(toChanges(confirmed, draft)).toEqual([
261
+ {
262
+ kind: "put_cell",
263
+ base_etag: null,
264
+ cell: { id: "c3", code: "3", name: "_", config: CONFIG },
265
+ after: "c2",
266
+ },
267
+ { kind: "delete_cell", id: "c1", base_etag: "c1-etag" },
268
+ { kind: "set_order", base_etag: "order-1", order: ["c2", "c3"] },
269
+ ]);
270
+ });
271
+ });
272
+
273
+ describe("acknowledge", () => {
274
+ it("releases accepted fields independently, keeping later code and config edits", () => {
275
+ const sent = edits({
276
+ code: "sent",
277
+ config: { hide_code: true, column: 1 },
278
+ });
279
+ const later = editCell(confirmed, sent, "c1", {
280
+ code: "typed more",
281
+ config: { column: 2 },
282
+ });
283
+ const remote = withCells(confirmed, {
284
+ c1: cell("c1", "remote", { config: { ...CONFIG, column: 1 } }),
285
+ });
286
+ expect(
287
+ viewDocument(remote, acknowledge(later, sent)).cells.get("c1"),
288
+ ).toEqual({
289
+ id: "c1",
290
+ code: "typed more",
291
+ name: "_",
292
+ config: { ...CONFIG, column: 2 },
293
+ });
294
+ });
295
+
296
+ it("turns an accepted creation into an edit of only the fields changed since", () => {
297
+ const sent = createCell(EMPTY_DRAFT, { id: "c3", code: "3" });
298
+ const later = editCell(confirmed, sent, "c3", { name: "named" });
299
+ const remote = withCells(confirmed, { c3: cell("c3", "remote code") });
300
+ expect(toChanges(remote, acknowledge(later, sent))).toEqual([
301
+ {
302
+ kind: "put_cell",
303
+ base_etag: "c3-etag",
304
+ cell: { id: "c3", code: "remote code", name: "named", config: CONFIG },
305
+ },
306
+ ]);
307
+ });
308
+
309
+ it("releases an accepted order but preserves a subsequent reorder", () => {
310
+ const sent = setOrder(EMPTY_DRAFT, ["c2", "c1"]);
311
+ const later = setOrder(sent, ["c1", "c2"]);
312
+ const remote = withCells(confirmed, {}, ["c2", "c1"]);
313
+ expect(toChanges(remote, acknowledge(sent, sent))).toEqual([]);
314
+ expect(toChanges(remote, acknowledge(later, sent))).toEqual([
315
+ {
316
+ kind: "set_order",
317
+ base_etag: remote.revision.order_etag,
318
+ order: ["c1", "c2"],
319
+ },
320
+ ]);
321
+ });
322
+ });