@marimo-team/islands 0.25.3-dev3 → 0.25.3-dev4

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.
@@ -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-BH_r7g7l.js";
5
- import { a as o, i as s, n as c, r as l } from "./glide-data-editor-BQ7uKbeo.js";
5
+ import { a as o, i as s, n as c, r as l } from "./glide-data-editor-Bv75MH6g.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_aca11a353ce3b112922852200b9c160f/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-Ft7t9bqz.js";
6
6
  import { Ct as o, Ft as s, H as c, L as l, Lt as u, V as d, d as f, un as p } from "./cells-C8iCYvLU.js";
7
7
  import { b as m } from "./select-BButFi-d.js";
8
- import { n as h, t as ee } from "./globals-DSavh2ZB.js";
8
+ import { n as h, t as ee } from "./globals-EDQGFIvi.js";
9
9
  import { t as g } from "./jsx-runtime-3BD1kT0s.js";
10
10
  import { C as _, _ as te, i as v } from "./useTheme-CRnjkku0.js";
11
11
  import { a as ne, i as re, n as ie, r as ae } from "./dialog-CEBFZNyc.js";
12
12
  import { t as oe } from "./useAsyncData-Deh5kszT.js";
13
- import { t as se } from "./code-visibility-BCpXa0Xh.js";
13
+ import { t as se } from "./code-visibility-Lu0dkrFO.js";
14
14
  import { i as ce, n as le } from "./error-entries-CzoYkIQZ.js";
15
15
  //#region src/components/ui/skeleton.tsx
16
16
  var y = /* @__PURE__ */ e(r(), 1), b = i(), x = g(), ue = (e) => {
@@ -6,7 +6,7 @@ import { i as s, n as c, r as l } from "./copy-icon-Ft7t9bqz.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-BButFi-d.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-CP_qCYHC.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-DJyqh0EM.js";
9
- import { n as ne, t as re } from "./ErrorBoundary-iHE9DnZC.js";
9
+ import { n as ne, t as re } from "./ErrorBoundary-CuU5CuVr.js";
10
10
  import { t as M } from "./react-dom-DUmH0GXv.js";
11
11
  import { t as N } from "./jsx-runtime-3BD1kT0s.js";
12
12
  import { t as ie } from "./label-BDKF3Afc.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_aca11a353ce3b112922852200b9c160f/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-Ch2SgmvG.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-ga7ytgbj.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_aca11a353ce3b112922852200b9c160f/node_modules/@glideapps/glide-data-grid/dist/esm/cells/number-cell.js
9296
- var Hs = K.lazy(async () => await import("./number-overlay-editor-D6kYaqh5.js")), Us = {
9296
+ var Hs = K.lazy(async () => await import("./number-overlay-editor-BCP3MxhC.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-BO1REbqs.js";
2
2
  import { _ as t } from "./useTheme-CRnjkku0.js";
3
- import { r as n } from "./code-visibility-BCpXa0Xh.js";
3
+ import { r as n } from "./code-visibility-Lu0dkrFO.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-BO1REbqs.js";
8
8
  import { $ as me, $t as he, B as ge, Bt as _e, C as ve, Ct as ye, D as be, Dt as xe, Et as Se, F as Ce, G as we, Gt as Te, Ht as Ee, I as De, J as Oe, Jt as ke, Kt as Ae, L as je, O as Me, Ot as Ne, P as Pe, Pt as Fe, Q as Ie, Qt as Le, Rt as Re, S as ze, St as Be, T as Ve, Tt as He, U as Ue, W as We, Wt as Ge, bt as Ke, c as qe, cn as Je, ct as Ye, d as Xe, dn as Ze, dt as Qe, en as $e, f as et, fn as tt, ft as nt, g as rt, gt as it, h as at, hn as ot, k as st, l as ct, ln as lt, m as ut, mn as dt, n as ft, o as pt, on as mt, ot as ht, p as gt, q as _t, qt as vt, r as yt, rn as bt, rt as xt, sn as St, st as Ct, tn as wt, u as Tt, v as Et, w as Dt, wt as Ot, zt as kt } from "./cells-C8iCYvLU.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-BwarBpUJ.js";
10
- import { $t as nn, A as rn, At as an, B as on, Bt as sn, C as cn, Ct as ln, D as un, Dt as dn, E as fn, Et as pn, F as mn, Ft as hn, G as gn, Gt as _n, H as vn, Ht as yn, I as bn, It as xn, J as Sn, Jt as Cn, K as wn, Kt as Tn, L as En, Lt as Dn, M as On, Mt as kn, N as An, Nt as jn, O as Mn, Ot as Nn, P as Pn, Pt as Fn, Qt as In, R as Ln, Rt as Rn, S as zn, St as Bn, T as Vn, Tt as Hn, U as Un, Ut as Wn, V as Gn, Vt as Kn, W as qn, Wt as Jn, X as Yn, Xt as Xn, Y as Zn, Yt as Qn, Zt as $n, _ as er, _t as tr, an as nr, at as rr, b as ir, c as ar, cn as or, ct as sr, d as cr, dn as lr, dt as ur, en as dr, et as fr, f as pr, fn as mr, ft as hr, g as gr, gt as _r, h as vr, hn as yr, ht as br, in as xr, it as Sr, j as Cr, jt as wr, k as Tr, kt as Er, ln as Dr, lt as Or, m as kr, mn as Ar, mt as jr, nn as Mr, nt as Nr, ot as Pr, p as Fr, pn as Ir, pt as Lr, q as Rr, qt as zr, r as Br, rn as Vr, rt as Hr, sn as Ur, st as Wr, t as Gr, tn as Kr, tt as qr, u as Jr, un as Yr, ut as Xr, v as Zr, vt as Qr, w as $r, wt as ei, x as ti, y as ni, yt as ri, z as ii, zt as ai } from "./common-BrBUdtPy.js";
10
+ import { $t as nn, A as rn, At as an, B as on, Bt as sn, C as cn, Ct as ln, D as un, Dt as dn, E as fn, Et as pn, F as mn, Ft as hn, G as gn, Gt as _n, H as vn, Ht as yn, I as bn, It as xn, J as Sn, Jt as Cn, K as wn, Kt as Tn, L as En, Lt as Dn, M as On, Mt as kn, N as An, Nt as jn, O as Mn, Ot as Nn, P as Pn, Pt as Fn, Qt as In, R as Ln, Rt as Rn, S as zn, St as Bn, T as Vn, Tt as Hn, U as Un, Ut as Wn, V as Gn, Vt as Kn, W as qn, Wt as Jn, X as Yn, Xt as Xn, Y as Zn, Yt as Qn, Zt as $n, _ as er, _t as tr, an as nr, at as rr, b as ir, c as ar, cn as or, ct as sr, d as cr, dn as lr, dt as ur, en as dr, et as fr, f as pr, fn as mr, ft as hr, g as gr, gt as _r, h as vr, hn as yr, ht as br, in as xr, it as Sr, j as Cr, jt as wr, k as Tr, kt as Er, ln as Dr, lt as Or, m as kr, mn as Ar, mt as jr, nn as Mr, nt as Nr, ot as Pr, p as Fr, pn as Ir, pt as Lr, q as Rr, qt as zr, r as Br, rn as Vr, rt as Hr, sn as Ur, st as Wr, t as Gr, tn as Kr, tt as qr, u as Jr, un as Yr, ut as Xr, v as Zr, vt as Qr, w as $r, wt as ei, x as ti, y as ni, yt as ri, z as ii, zt as ai } from "./common-DiWGWDIM.js";
11
11
  import { c as oi, i as si, n as ci, o as li, t as ui } from "./useLifecycle-j7D9uD1o.js";
12
12
  import { t as di } from "./check-EDwSwP89.js";
13
13
  import { A as fi, C as pi, D as mi, E as hi, M as gi, O as _i, S as vi, T as yi, a as bi, b as xi, c as Si, i as Ci, k as wi, l as Ti, n as Ei, o as Di, r as Oi, s as ki, t as Ai, w as ji, x as Mi } from "./select-BButFi-d.js";
@@ -15,8 +15,8 @@ import { $ as Ni, C as Pi, Et as Fi, J as Ii, K as Li, R as Ri, S as zi, U as Bi
15
15
  import { S as ua, a as da, b as fa, c as pa, d as ma, f as ha, g as ga, l as _a, m as va, p as ya, s as ba, v as xa, y as Sa } from "./toDate-BNJf01u1.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-e4jj9Ijt.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-DJyqh0EM.js";
18
- import { n as Va, t as Ha } from "./ErrorBoundary-iHE9DnZC.js";
19
- import { n as Ua, t as Wa } from "./globals-DSavh2ZB.js";
18
+ import { n as Va, t as Ha } from "./ErrorBoundary-CuU5CuVr.js";
19
+ import { n as Ua, t as Wa } from "./globals-EDQGFIvi.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-BEM7sFWl.js";
@@ -39,7 +39,7 @@ import { t as Ms } from "./useAsyncData-Deh5kszT.js";
39
39
  import { i as Ns, n as Ps, r as Fs } from "./formats-BGh2thRc.js";
40
40
  import { M as Is, N as Ls } from "./vega-loader.browser-DNST6uVf.js";
41
41
  import { a as Rs, n as zs } from "./loader-BQOfjTIJ.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-BCpXa0Xh.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-Lu0dkrFO.js";
43
43
  import { t as Js } from "./useDeepCompareMemoize-DXwzRINI.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
@@ -10533,7 +10533,7 @@ function VC(e, t) {
10533
10533
  }
10534
10534
  //#endregion
10535
10535
  //#region src/plugins/impl/DataEditorPlugin.tsx
10536
- var HC = J.lazy(() => import("./glide-data-editor-BQ7uKbeo.js").then((e) => e.t)), UC = wS("marimo-data-editor", { cssStyles: [qS] }).withData(W({
10536
+ var HC = J.lazy(() => import("./glide-data-editor-Bv75MH6g.js").then((e) => e.t)), UC = wS("marimo-data-editor", { cssStyles: [qS] }).withData(W({
10537
10537
  initialValue: W({ edits: G(W({
10538
10538
  rowIdx: K(),
10539
10539
  columnId: U(),
@@ -30107,7 +30107,7 @@ function OR(e) {
30107
30107
  }
30108
30108
  //#endregion
30109
30109
  //#region src/components/editor/renderers/slides-layout/slides-layout.tsx
30110
- var kR = J.lazy(() => import("./reveal-component-BTBfJ3ju.js")), AR = (e) => {
30110
+ var kR = J.lazy(() => import("./reveal-component-DO5BJB9l.js")), AR = (e) => {
30111
30111
  let t = (0, X.c)(33), { layout: n, setLayout: r, cells: i, mode: a } = e, o = ns(ga), s = a === "read" || o, c = ns(gt) > 1, [l, u] = (0, J.useState)(null), d;
30112
30112
  t[0] !== i || t[1] !== n ? (d = EP(i, n), t[0] = i, t[1] = n, t[2] = d) : d = t[2];
30113
30113
  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-BQ7uKbeo.js";
3
+ import { a as n } from "./glide-data-editor-Bv75MH6g.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_aca11a353ce3b112922852200b9c160f/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-BO1REbqs.js";
6
6
  import { w as u } from "./cells-C8iCYvLU.js";
7
- import { $ as d, Q as f, Z as p, an as m, i as h, l as g, ln as _, n as v, on as y, r as b, t as x, u as S } from "./common-BrBUdtPy.js";
7
+ import { $ as d, Q as f, Z as p, an as m, i as h, l as g, ln as _, n as v, on as y, r as b, t as x, u as S } from "./common-DiWGWDIM.js";
8
8
  import { M as C } from "./select-BButFi-d.js";
9
9
  import { q as ee } from "./dropdown-menu-CP_qCYHC.js";
10
10
  import { g as w } from "./toDate-BNJf01u1.js";
11
11
  import { t as T } from "./jsx-runtime-3BD1kT0s.js";
12
12
  import { q as E } from "./use-restore-focus-BEM7sFWl.js";
13
13
  import { A as D, C as O, b as k } from "./useTheme-CRnjkku0.js";
14
- import { a as A, n as te, t as ne } from "./code-visibility-BCpXa0Xh.js";
14
+ import { a as A, n as te, t as ne } from "./code-visibility-Lu0dkrFO.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-dev3",
3
+ "version": "0.25.3-dev4",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -0,0 +1,176 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
4
+ import { buildCellFilter } from "../context-menu";
5
+
6
+ describe("buildCellFilter", () => {
7
+ // Use a non-UTC, non-DST-ambiguous offset so date-only parsing bugs
8
+ // (UTC vs. local midnight) can't hide behind a host machine that
9
+ // happens to run in UTC.
10
+ let originalTimezone: string | undefined;
11
+ beforeAll(() => {
12
+ originalTimezone = process.env.TZ;
13
+ process.env.TZ = "America/New_York";
14
+ });
15
+ afterAll(() => {
16
+ process.env.TZ = originalTimezone;
17
+ });
18
+
19
+ it("builds an equality filter for a normal date value", () => {
20
+ const filter = buildCellFilter({
21
+ type: "date",
22
+ value: "2024-01-15",
23
+ comparison: "eq",
24
+ dataType: "date",
25
+ dtype: "date",
26
+ });
27
+ expect(filter).toEqual({
28
+ type: "date",
29
+ operator: "==",
30
+ // Must be local midnight, not `new Date("2024-01-15")` (UTC
31
+ // midnight), or the filter shifts by a day in UTC- timezones.
32
+ value: new Date(2024, 0, 15),
33
+ });
34
+ });
35
+
36
+ it("returns undefined for an empty-string date value in a non-temporal-looking context", () => {
37
+ const filter = buildCellFilter({
38
+ type: "date",
39
+ value: "",
40
+ comparison: "eq",
41
+ dataType: "date",
42
+ dtype: "date",
43
+ });
44
+ expect(filter).toBeUndefined();
45
+ });
46
+
47
+ it("returns undefined for an unparsable date value instead of an Invalid Date filter", () => {
48
+ const filter = buildCellFilter({
49
+ type: "date",
50
+ value: "not-a-date",
51
+ comparison: "eq",
52
+ dataType: "date",
53
+ dtype: "date",
54
+ });
55
+ expect(filter).toBeUndefined();
56
+ });
57
+
58
+ it("builds an equality filter for a normal datetime value", () => {
59
+ const filter = buildCellFilter({
60
+ type: "datetime",
61
+ value: "2024-01-15 08:30:00",
62
+ comparison: "eq",
63
+ dataType: "datetime",
64
+ dtype: "datetime64[ns]",
65
+ });
66
+ expect(filter).toEqual({
67
+ type: "datetime",
68
+ operator: "==",
69
+ value: new Date(2024, 0, 15, 8, 30, 0),
70
+ });
71
+ });
72
+
73
+ it("returns undefined for an unparsable datetime value", () => {
74
+ const filter = buildCellFilter({
75
+ type: "datetime",
76
+ value: "not-a-datetime",
77
+ comparison: "eq",
78
+ dataType: "datetime",
79
+ dtype: "datetime64[ns]",
80
+ });
81
+ expect(filter).toBeUndefined();
82
+ });
83
+
84
+ it("builds an equality filter for a normal time value", () => {
85
+ const filter = buildCellFilter({
86
+ type: "time",
87
+ value: "08:30:00",
88
+ comparison: "eq",
89
+ dataType: "time",
90
+ });
91
+ expect(filter).toEqual({
92
+ type: "time",
93
+ operator: "==",
94
+ value: new Date(1970, 0, 1, 8, 30, 0),
95
+ });
96
+ });
97
+
98
+ it("returns undefined instead of an unparsable time value", () => {
99
+ const filter = buildCellFilter({
100
+ type: "time",
101
+ value: "25:00:00",
102
+ comparison: "eq",
103
+ dataType: "time",
104
+ });
105
+ expect(filter).toBeUndefined();
106
+ });
107
+
108
+ it("maps a NaT date value to is_null instead of an invalid Date filter", () => {
109
+ const filter = buildCellFilter({
110
+ type: "date",
111
+ value: "NaT",
112
+ comparison: "eq",
113
+ dataType: "date",
114
+ dtype: "date",
115
+ });
116
+ expect(filter).toEqual({ type: "date", operator: "is_null" });
117
+ });
118
+
119
+ it("maps a NaT datetime value to is_not_null for the neq comparison", () => {
120
+ const filter = buildCellFilter({
121
+ type: "datetime",
122
+ value: "NaT",
123
+ comparison: "neq",
124
+ dataType: "datetime",
125
+ dtype: "datetime64[ns]",
126
+ });
127
+ expect(filter).toEqual({ type: "datetime", operator: "is_not_null" });
128
+ });
129
+
130
+ it("does not treat the string 'NaT' in a non-temporal column as a sentinel", () => {
131
+ const filter = buildCellFilter({
132
+ type: "text",
133
+ value: "NaT",
134
+ comparison: "eq",
135
+ dataType: "string",
136
+ });
137
+ expect(filter).toEqual({ type: "text", operator: "equals", text: "NaT" });
138
+ });
139
+
140
+ it("maps an actual null value to is_null", () => {
141
+ const filter = buildCellFilter({
142
+ type: "number",
143
+ value: null,
144
+ comparison: "eq",
145
+ dataType: "number",
146
+ });
147
+ expect(filter).toEqual({ type: "number", operator: "is_null" });
148
+ });
149
+
150
+ it("builds a literal (always-false) equality filter for a NaN number value", () => {
151
+ // NaN is a real float value, not a missing-value marker like null/NaT,
152
+ // and the backend's is_null() (narwhals/polars semantics) doesn't match
153
+ // it, so it falls through to a normal (if unsatisfiable) == filter.
154
+ const filter = buildCellFilter({
155
+ type: "number",
156
+ value: Number.NaN,
157
+ comparison: "eq",
158
+ dataType: "number",
159
+ });
160
+ expect(filter).toEqual({
161
+ type: "number",
162
+ operator: "==",
163
+ value: Number.NaN,
164
+ });
165
+ });
166
+
167
+ it("builds a literal equality filter for a normal number value", () => {
168
+ const filter = buildCellFilter({
169
+ type: "number",
170
+ value: 42,
171
+ comparison: "eq",
172
+ dataType: "number",
173
+ });
174
+ expect(filter).toEqual({ type: "number", operator: "==", value: 42 });
175
+ });
176
+ });
@@ -1,7 +1,30 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import { describe, expect, it } from "vitest";
3
3
  import { dateToLocalISODate } from "@/utils/dates";
4
- import { parsePastedRange } from "../date-filter-inputs";
4
+ import { parsePastedDate, parsePastedRange } from "../date-filter-inputs";
5
+
6
+ describe("parsePastedDate", () => {
7
+ it("parses a time-only string for the time filter type", () => {
8
+ const result = parsePastedDate("time", "2:30 PM");
9
+ expect(result).toEqual(new Date(1970, 0, 1, 14, 30));
10
+ });
11
+
12
+ it("parses a local ISO datetime for the datetime filter type", () => {
13
+ const result = parsePastedDate("datetime", "2026-02-01T08:30:00");
14
+ expect(result).toEqual(new Date(2026, 1, 1, 8, 30, 0));
15
+ });
16
+
17
+ it("falls back to the Date constructor for other formats", () => {
18
+ const result = parsePastedDate("datetime", "Feb 1, 2026 08:30:00");
19
+ expect(result).toBeDefined();
20
+ expect(result?.getFullYear()).toBe(2026);
21
+ });
22
+
23
+ it("returns undefined for an empty or unparsable string", () => {
24
+ expect(parsePastedDate("date", "")).toBeUndefined();
25
+ expect(parsePastedDate("time", "not-a-time")).toBeUndefined();
26
+ });
27
+ });
5
28
 
6
29
  describe("parsePastedRange", () => {
7
30
  it.each([
@@ -1,12 +1,19 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
+ import type { DataType } from "@/core/kernel/messages";
4
+ import { assertNever } from "@/utils/assertNever";
5
+ import { copyToClipboard } from "@/utils/copy";
6
+ import {
7
+ parseLocalISODate,
8
+ parseLocalISODateTime,
9
+ parseTimeToDate,
10
+ } from "@/utils/dates";
11
+ import { Logger } from "@/utils/Logger";
3
12
  import type { Cell } from "@tanstack/react-table";
4
13
  import { useAtomValue } from "jotai";
5
14
  import { CopyIcon, FilterIcon, SquareStack } from "lucide-react";
6
15
  import type { RefObject } from "react";
7
16
  import useEvent from "react-use-event-hook";
8
- import { copyToClipboard } from "@/utils/copy";
9
- import { Logger } from "@/utils/Logger";
10
17
  import {
11
18
  ContextMenu,
12
19
  ContextMenuContent,
@@ -16,13 +23,94 @@ import {
16
23
  ContextMenuTrigger,
17
24
  } from "../ui/context-menu";
18
25
  import { DATA_CELL_ID } from "./cell-utils";
19
- import {
20
- Filter,
21
- isMembershipFilterType,
22
- type MembershipFilterType,
23
- } from "./filters";
26
+ import { type FilterType, Filter } from "./filters";
24
27
  import { selectedCellsAtom } from "./range-focus/atoms";
25
- import { getClipboardContent, getRawValue } from "./utils";
28
+ import { getClipboardContent, getRawValue, isNullishSentinel } from "./utils";
29
+
30
+ type Comparison = "eq" | "neq";
31
+
32
+ const symbolOp = {
33
+ eq: "==",
34
+ neq: "!=",
35
+ } as const;
36
+
37
+ const textOp = {
38
+ eq: "equals",
39
+ neq: "does_not_equal",
40
+ } as const;
41
+
42
+ export function buildCellFilter({
43
+ type,
44
+ value,
45
+ comparison = "eq",
46
+ dataType,
47
+ dtype,
48
+ }: {
49
+ type: FilterType;
50
+ value: unknown;
51
+ comparison?: Comparison;
52
+ dataType?: DataType;
53
+ dtype?: string;
54
+ }) {
55
+ if (isNullishSentinel(value, dataType, dtype)) {
56
+ return { type, operator: comparison === "eq" ? "is_null" : "is_not_null" };
57
+ }
58
+
59
+ switch (type) {
60
+ case "number":
61
+ if (typeof value !== "number") {
62
+ return undefined;
63
+ }
64
+
65
+ return Filter.number({
66
+ operator: symbolOp[comparison],
67
+ value,
68
+ });
69
+ case "text":
70
+ return Filter.text({
71
+ operator: textOp[comparison],
72
+ text: value as string,
73
+ });
74
+ case "date": {
75
+ const date = parseLocalISODate(value as string);
76
+ if (!date) {
77
+ return undefined;
78
+ }
79
+ return Filter.date({
80
+ operator: symbolOp[comparison],
81
+ value: date,
82
+ });
83
+ }
84
+ case "datetime": {
85
+ const date = parseLocalISODateTime(value as string);
86
+ if (!date) {
87
+ return undefined;
88
+ }
89
+ return Filter.datetime({
90
+ operator: symbolOp[comparison],
91
+ value: date,
92
+ });
93
+ }
94
+ case "time": {
95
+ const time = parseTimeToDate(value as string);
96
+ if (!time) {
97
+ return undefined;
98
+ }
99
+ return Filter.time({
100
+ operator: symbolOp[comparison],
101
+ value: time,
102
+ });
103
+ }
104
+ case "boolean": {
105
+ const target = comparison === "eq" ? value : !(value as boolean);
106
+ return Filter.boolean({
107
+ operator: target ? "is_true" : "is_false",
108
+ });
109
+ }
110
+ default:
111
+ assertNever(type);
112
+ }
113
+ }
26
114
 
27
115
  export const DataTableContextMenu = <TData,>({
28
116
  contextMenuRef,
@@ -102,19 +190,34 @@ export const CellContextMenu = <TData,>({
102
190
 
103
191
  const column = cell.column;
104
192
  const filterType = column.columnDef.meta?.filterType;
105
- const membershipFilterType: MembershipFilterType | undefined =
106
- column.getCanFilter() && filterType && isMembershipFilterType(filterType)
107
- ? filterType
193
+ const dataType = column.columnDef.meta?.dataType;
194
+ const dtype = column.columnDef.meta?.dtype;
195
+
196
+ const canFilter = Boolean(filterType && column.getCanFilter());
197
+ const buildFilter = (comparison: Comparison) =>
198
+ canFilter
199
+ ? buildCellFilter({
200
+ type: filterType as FilterType,
201
+ value: rawValue,
202
+ comparison,
203
+ dataType,
204
+ dtype,
205
+ })
108
206
  : undefined;
207
+ const eqFilter = buildFilter("eq");
208
+ const canFilterByValue = eqFilter !== undefined;
209
+
210
+ const handleFilterCellByValue = (comparison: Comparison) => {
211
+ if (!filterType) {
212
+ return;
213
+ }
214
+
215
+ const filter = comparison === "eq" ? eqFilter : buildFilter("neq");
216
+
217
+ if (!filter) {
218
+ return;
219
+ }
109
220
 
110
- const handleFilterCell = (
111
- type: MembershipFilterType,
112
- operator: "in" | "not_in",
113
- ) => {
114
- const filter =
115
- type === "number"
116
- ? Filter.number({ operator, values: [rawValue] })
117
- : Filter.text({ operator, values: [rawValue] });
118
221
  column.setFilterValue(filter);
119
222
  };
120
223
 
@@ -130,18 +233,14 @@ export const CellContextMenu = <TData,>({
130
233
  Copy selected cells
131
234
  </ContextMenuItem>
132
235
  )}
133
- {membershipFilterType && (
236
+ {canFilterByValue && (
134
237
  <>
135
238
  <ContextMenuSeparator />
136
- <ContextMenuItem
137
- onClick={() => handleFilterCell(membershipFilterType, "in")}
138
- >
239
+ <ContextMenuItem onClick={() => handleFilterCellByValue("eq")}>
139
240
  <FilterIcon className="mo-dropdown-icon h-3 w-3" />
140
241
  Filter by this value
141
242
  </ContextMenuItem>
142
- <ContextMenuItem
143
- onClick={() => handleFilterCell(membershipFilterType, "not_in")}
144
- >
243
+ <ContextMenuItem onClick={() => handleFilterCellByValue("neq")}>
145
244
  <FilterIcon className="mo-dropdown-icon h-3 w-3" />
146
245
  Remove rows with this value
147
246
  </ContextMenuItem>
@@ -14,6 +14,9 @@ import {
14
14
  dateToLocalISODate,
15
15
  dateToLocalISODateTime,
16
16
  dateToLocalISOTime,
17
+ parseLocalISODate,
18
+ parseLocalISODateTime,
19
+ parseTimeToDate,
17
20
  } from "@/utils/dates";
18
21
  import type { FilterType } from "./filters";
19
22
 
@@ -72,53 +75,23 @@ export function parsePastedDate(
72
75
  return undefined;
73
76
  }
74
77
 
75
- const timeMatch =
76
- filterType === "time"
77
- ? trimmed.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?\s*(am|pm)?$/i)
78
- : null;
79
- if (timeMatch) {
80
- const [, hStr, mStr, sStr, ampm] = timeMatch;
81
- let hour = Number.parseInt(hStr, 10);
82
- const minute = Number.parseInt(mStr, 10);
83
- const second = sStr ? Number.parseInt(sStr, 10) : 0;
84
- if (ampm) {
85
- const isPm = ampm.toLowerCase() === "pm";
86
- if (hour === 12) {
87
- hour = isPm ? 12 : 0;
88
- } else if (isPm) {
89
- hour += 12;
90
- }
78
+ if (filterType === "time") {
79
+ const date = parseTimeToDate(trimmed);
80
+ if (date) {
81
+ return date;
91
82
  }
92
- return new Date(1970, 0, 1, hour, minute, second);
93
83
  }
94
84
 
95
- const dateOnlyMatch = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
96
- if (dateOnlyMatch) {
97
- const [, y, m, d] = dateOnlyMatch;
98
- return new Date(
99
- Number.parseInt(y, 10),
100
- Number.parseInt(m, 10) - 1,
101
- Number.parseInt(d, 10),
102
- );
85
+ if (filterType === "date") {
86
+ const date = parseLocalISODate(trimmed);
87
+ if (date) {
88
+ return date;
89
+ }
103
90
  }
104
91
 
105
- // Parse ISO datetimes as wall-clock to stay consistent with the picker's
106
- // local-time basis. Trailing `Z` or offsets are stripped so that pasting
107
- // `2024-01-15T08:30:00Z` displays `08:30` instead of being shifted by the
108
- // viewer's timezone.
109
- const isoMatch = trimmed.match(
110
- /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?$/,
111
- );
112
- if (isoMatch) {
113
- const [, y, mo, d, h, mi, s] = isoMatch;
114
- return new Date(
115
- Number.parseInt(y, 10),
116
- Number.parseInt(mo, 10) - 1,
117
- Number.parseInt(d, 10),
118
- Number.parseInt(h, 10),
119
- Number.parseInt(mi, 10),
120
- s ? Number.parseInt(s, 10) : 0,
121
- );
92
+ const isoDateTime = parseLocalISODateTime(trimmed);
93
+ if (isoDateTime) {
94
+ return isoDateTime;
122
95
  }
123
96
 
124
97
  const parsed = new Date(trimmed);
@@ -228,6 +228,24 @@ export function detectSentinel(
228
228
  return null;
229
229
  }
230
230
 
231
+ type NullishSentinelType = Extract<CellValueSentinel["type"], "null" | "nat">;
232
+
233
+ const NULLISH_SENTINEL_TYPES: ReadonlySet<CellValueSentinel["type"]> =
234
+ new Set<NullishSentinelType>(["null", "nat"]);
235
+
236
+ /**
237
+ * True when the cell value represents a missing value (null, NaT)
238
+ * rather than a real value that happens to look special.
239
+ */
240
+ export function isNullishSentinel(
241
+ value: unknown,
242
+ dataType: DataType | undefined,
243
+ dtype?: string,
244
+ ): boolean {
245
+ const sentinel = detectSentinel(value, dataType, dtype);
246
+ return sentinel !== null && NULLISH_SENTINEL_TYPES.has(sentinel.type);
247
+ }
248
+
231
249
  /**
232
250
  * Stringify an unknown value. Converts objects to JSON strings.
233
251
  * @param opts.value - The value to stringify.