@marimo-team/islands 0.23.7-dev9 → 0.23.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/dist/{ConnectedDataExplorerComponent-DnRhpPMJ.js → ConnectedDataExplorerComponent-2lBNiUv6.js} +13 -13
  2. package/dist/{ErrorBoundary-Da4UeYxT.js → ErrorBoundary-D3wrPNma.js} +1 -1
  3. package/dist/{any-language-editor-DDubl8YH.js → any-language-editor-VWs_7v27.js} +5 -5
  4. package/dist/assets/__vite-browser-external-CAdMKBac.js +1 -0
  5. package/dist/assets/worker-CpBbwbQo.js +73 -0
  6. package/dist/{button-CA5pI2YF.js → button-Dj4BTre0.js} +5 -0
  7. package/dist/{capabilities-6laDasij.js → capabilities-C9rrYCzf.js} +1 -1
  8. package/dist/{chat-ui-BmWZZ3mE.js → chat-ui-D3XBept8.js} +625 -233
  9. package/dist/{check-CFM2mVDr.js → check-BcUIXnUT.js} +1 -1
  10. package/dist/{code-visibility-CRHzv49w.js → code-visibility-sKGUbHmr.js} +11480 -1992
  11. package/dist/{copy-TGGAUEWp.js → copy-DLf4aN7I.js} +2 -2
  12. package/dist/{dist-ESg7xyoD.js → dist-D3ZI9nhS.js} +2 -2
  13. package/dist/{error-banner-DnBPzEWg.js → error-banner-CVkfBUT3.js} +2 -2
  14. package/dist/{esm-Dd1z1auZ.js → esm-CWp0KQeK.js} +1 -1
  15. package/dist/{extends-CzJgxo2J.js → extends-vAi97cpa.js} +4 -4
  16. package/dist/{formats-CgaK7Gmx.js → formats-Dsy9kkZu.js} +3 -3
  17. package/dist/{glide-data-editor-B-3A3G02.js → glide-data-editor-DucgdjRo.js} +9 -9
  18. package/dist/{html-to-image-BwZL1Pkk.js → html-to-image-CpggM7u1.js} +2667 -2408
  19. package/dist/{input-BAOe64zx.js → input-D4kjoQUB.js} +8 -6
  20. package/dist/{label-BCWi-Oqu.js → label-BLqV33b1.js} +2 -2
  21. package/dist/{loader-BvW0-YWZ.js → loader-Dr8Qem8p.js} +1 -1
  22. package/dist/main.js +1697 -10282
  23. package/dist/{mermaid-cXSZ1pfD.js → mermaid-DO-Daq7u.js} +5 -5
  24. package/dist/{process-output-lpVrk7d5.js → process-output-X8TR20AK.js} +3 -3
  25. package/dist/reveal-component-BBAxPTso.js +7447 -0
  26. package/dist/{spec-DSIuqd3f.js → spec-hVaaZsY5.js} +4 -4
  27. package/dist/{strings-B_FOH6eV.js → strings-BiIhGaI8.js} +4 -4
  28. package/dist/style.css +1 -1
  29. package/dist/{swiper-component-BHs0PWwp.js → swiper-component-DlD2GU2g.js} +2 -2
  30. package/dist/{toDate-CHtl9vts.js → toDate-CIpC_34u.js} +33 -20
  31. package/dist/{tooltip-B0mtKTXm.js → tooltip-DRaMBu06.js} +3 -3
  32. package/dist/{types-DBtDeUKD.js → types-Dzuoc3LN.js} +1 -1
  33. package/dist/{useAsyncData-B6hCGywC.js → useAsyncData-C56Khv_R.js} +1 -1
  34. package/dist/{useDateFormatter-B3mCQMP3.js → useDateFormatter-B_9k85Ex.js} +2 -2
  35. package/dist/{useDeepCompareMemoize-CmwDuYUH.js → useDeepCompareMemoize-Dt98v2ua.js} +1 -1
  36. package/dist/{useIframeCapabilities-DbdLoEDm.js → useIframeCapabilities-BkYHTrss.js} +1 -1
  37. package/dist/{useLifecycle-CjMjllqy.js → useLifecycle-BF6-z62y.js} +3 -3
  38. package/dist/{useTheme-CByZUW0p.js → useTheme-DykuNHR2.js} +2 -2
  39. package/dist/{vega-component-C2BYPkfd.js → vega-component-cSdqoAxe.js} +10 -10
  40. package/dist/{zod-BxdsqRPd.js → zod-BWkcDORu.js} +1 -1
  41. package/package.json +3 -3
  42. package/src/components/chat/chat-components.tsx +47 -0
  43. package/src/components/chat/chat-display.tsx +41 -7
  44. package/src/components/chat/chat-panel.tsx +37 -10
  45. package/src/components/chat/chat-utils.ts +42 -20
  46. package/src/components/chat/reasoning-accordion.tsx +14 -3
  47. package/src/components/chat/tool-call/shared.ts +13 -0
  48. package/src/components/chat/tool-call/tool-approval-card.tsx +62 -0
  49. package/src/components/chat/tool-call/tool-args.tsx +26 -0
  50. package/src/components/chat/tool-call/tool-call-view.tsx +99 -0
  51. package/src/components/chat/tool-call/tool-error-card.tsx +81 -0
  52. package/src/components/chat/tool-call/tool-history-row.tsx +153 -0
  53. package/src/components/chat/tool-call/tool-result.tsx +101 -0
  54. package/src/components/data-table/__tests__/column-header.test.ts +3 -1
  55. package/src/components/data-table/__tests__/column-header.test.tsx +308 -0
  56. package/src/components/data-table/__tests__/filter-by-values-picker.test.tsx +112 -0
  57. package/src/components/data-table/__tests__/filter-pill-editor.test.tsx +261 -0
  58. package/src/components/data-table/__tests__/filters.test.ts +196 -49
  59. package/src/components/data-table/charts/components/form-fields.tsx +1 -0
  60. package/src/components/data-table/column-header.tsx +349 -170
  61. package/src/components/data-table/date-filter-inputs.tsx +325 -0
  62. package/src/components/data-table/filter-by-values-picker.tsx +70 -9
  63. package/src/components/data-table/filter-pill-editor.tsx +410 -156
  64. package/src/components/data-table/filter-pills.tsx +69 -54
  65. package/src/components/data-table/filters.ts +218 -101
  66. package/src/components/data-table/header-items.tsx +8 -1
  67. package/src/components/data-table/operator-labels.ts +25 -0
  68. package/src/components/data-table/regex-input.tsx +61 -0
  69. package/src/components/dependency-graph/minimap-content.tsx +14 -3
  70. package/src/components/editor/actions/pair-with-agent-modal.tsx +140 -49
  71. package/src/components/editor/actions/useNotebookActions.tsx +3 -1
  72. package/src/components/editor/app-container.tsx +7 -1
  73. package/src/components/editor/chrome/panels/context-aware-panel/context-aware-panel.tsx +10 -2
  74. package/src/components/editor/chrome/wrapper/app-chrome.tsx +1 -0
  75. package/src/components/editor/chrome/wrapper/footer-items/backend-status.tsx +1 -1
  76. package/src/components/editor/chrome/wrapper/footer.tsx +4 -1
  77. package/src/components/editor/chrome/wrapper/panels.tsx +4 -1
  78. package/src/components/editor/chrome/wrapper/sidebar.tsx +4 -1
  79. package/src/components/editor/controls/Controls.tsx +11 -3
  80. package/src/components/editor/file-tree/file-explorer.tsx +12 -2
  81. package/src/components/editor/header/__tests__/status.test.tsx +108 -0
  82. package/src/components/editor/header/status.tsx +44 -10
  83. package/src/components/editor/navigation/__tests__/clipboard.test.ts +106 -0
  84. package/src/components/editor/navigation/__tests__/navigation.test.ts +70 -0
  85. package/src/components/editor/navigation/clipboard.ts +99 -25
  86. package/src/components/editor/navigation/navigation.ts +15 -1
  87. package/src/components/editor/notebook-cell.tsx +5 -0
  88. package/src/components/editor/output/console/ConsoleOutput.tsx +23 -5
  89. package/src/components/editor/output/console/__tests__/ConsoleOutput.test.tsx +114 -0
  90. package/src/components/editor/renderers/slides-layout/__tests__/compute-slide-cells.test.ts +5 -4
  91. package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +55 -15
  92. package/src/components/editor/renderers/slides-layout/plugin.tsx +8 -25
  93. package/src/components/editor/renderers/slides-layout/slides-layout.tsx +19 -6
  94. package/src/components/editor/renderers/slides-layout/types.ts +40 -31
  95. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +1 -0
  96. package/src/components/home/components.tsx +6 -0
  97. package/src/components/pages/run-page.tsx +4 -1
  98. package/src/components/scratchpad/scratchpad.tsx +1 -0
  99. package/src/components/slides/__tests__/slide-notes.test.ts +131 -0
  100. package/src/components/slides/reveal-component.tsx +252 -147
  101. package/src/components/slides/slide-notes-editor.tsx +127 -0
  102. package/src/components/slides/slide-notes.ts +64 -0
  103. package/src/components/slides/slides.css +14 -0
  104. package/src/components/ui/combobox.tsx +24 -5
  105. package/src/components/ui/number-field.tsx +2 -0
  106. package/src/core/ai/tools/__tests__/registry.test.ts +10 -12
  107. package/src/core/ai/tools/registry.ts +9 -5
  108. package/src/core/cells/__tests__/cells.test.ts +187 -0
  109. package/src/core/cells/__tests__/pending-cut-service.test.tsx +123 -0
  110. package/src/core/cells/cells.ts +102 -17
  111. package/src/core/cells/document-changes.ts +6 -1
  112. package/src/core/cells/pending-cut-service.ts +55 -0
  113. package/src/core/cells/utils.ts +11 -0
  114. package/src/core/codemirror/cells/extensions.ts +10 -0
  115. package/src/core/codemirror/go-to-definition/__tests__/commands.test.ts +152 -0
  116. package/src/core/codemirror/go-to-definition/__tests__/utils.test.ts +99 -0
  117. package/src/core/codemirror/go-to-definition/commands.ts +382 -22
  118. package/src/core/codemirror/go-to-definition/utils.ts +23 -5
  119. package/src/core/edit-app.tsx +3 -2
  120. package/src/core/hotkeys/hotkeys.ts +5 -0
  121. package/src/core/islands/worker/worker.tsx +3 -2
  122. package/src/core/run-app.tsx +2 -1
  123. package/src/core/runtime/__tests__/runtime.test.ts +38 -17
  124. package/src/core/runtime/runtime.ts +57 -34
  125. package/src/core/wasm/__tests__/utils.test.ts +34 -0
  126. package/src/core/wasm/utils.ts +14 -0
  127. package/src/core/wasm/worker/bootstrap.ts +3 -2
  128. package/src/core/wasm/worker/worker.ts +3 -2
  129. package/src/core/websocket/__tests__/useMarimoKernelConnection.hook.test.tsx +156 -0
  130. package/src/core/websocket/__tests__/useMarimoKernelConnection.test.ts +101 -0
  131. package/src/core/websocket/transports/__tests__/ws.test.ts +125 -0
  132. package/src/core/websocket/transports/basic.ts +1 -1
  133. package/src/core/websocket/transports/ws.ts +96 -0
  134. package/src/core/websocket/useMarimoKernelConnection.tsx +133 -54
  135. package/src/core/websocket/useWebSocket.tsx +3 -15
  136. package/src/css/app/Cell.css +10 -0
  137. package/src/plugins/core/__test__/sanitize.test.ts +30 -0
  138. package/src/plugins/impl/DropdownPlugin.tsx +12 -1
  139. package/src/plugins/impl/MultiselectPlugin.tsx +4 -0
  140. package/src/plugins/impl/SearchableSelect.tsx +11 -1
  141. package/src/plugins/impl/TabsPlugin.tsx +35 -7
  142. package/src/plugins/impl/__tests__/DropdownPlugin.test.tsx +56 -0
  143. package/src/plugins/impl/__tests__/TabsPlugin.test.tsx +154 -0
  144. package/src/plugins/impl/data-frames/forms/__tests__/__snapshots__/form.test.tsx.snap +48 -36
  145. package/src/plugins/impl/data-frames/schema.ts +4 -1
  146. package/src/plugins/layout/DownloadPlugin.tsx +9 -7
  147. package/src/utils/__tests__/id-tree.test.ts +71 -0
  148. package/src/utils/download.ts +4 -2
  149. package/src/utils/id-tree.tsx +89 -0
  150. package/dist/assets/__vite-browser-external-rrUYDKRl.js +0 -1
  151. package/dist/assets/worker-Bfy15ViQ.js +0 -73
  152. package/dist/reveal-component-C97Ceb7e.js +0 -4863
  153. package/src/components/chat/tool-call-accordion.tsx +0 -247
@@ -12,7 +12,12 @@ import { Badge } from "../ui/badge";
12
12
  import { Button } from "../ui/button";
13
13
  import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
14
14
  import { FilterPillEditor } from "./filter-pill-editor";
15
- import type { ColumnFilterValue } from "./filters";
15
+ import {
16
+ type ColumnFilterValue,
17
+ dateToISODate,
18
+ dateToISODateTime,
19
+ } from "./filters";
20
+ import { OPERATOR_LABELS } from "./operator-labels";
16
21
  import { stringifyUnknownValue } from "./utils";
17
22
 
18
23
  interface Props<TData> {
@@ -81,13 +86,6 @@ const FilterPill = <TData,>({
81
86
  return null;
82
87
  }
83
88
 
84
- // this is temporary, with more operator & datatype support this goes away
85
- const isReadOnly =
86
- "type" in value &&
87
- (value.type === "date" ||
88
- value.type === "datetime" ||
89
- value.type === "time");
90
-
91
89
  const twoSegment = formatted.value === undefined;
92
90
 
93
91
  const handleRemove = (e: React.MouseEvent) => {
@@ -129,20 +127,8 @@ const FilterPill = <TData,>({
129
127
  </Button>
130
128
  );
131
129
 
132
- if (isReadOnly) {
133
- return (
134
- <Badge
135
- variant="outline"
136
- className="bg-background border-border text-foreground"
137
- >
138
- {segments}
139
- {removeButton}
140
- </Badge>
141
- );
142
- }
143
-
144
130
  return (
145
- <Popover open={open} onOpenChange={setOpen} modal={true}>
131
+ <Popover open={open} onOpenChange={setOpen} modal={false}>
146
132
  <Badge
147
133
  variant="outline"
148
134
  className={cn(
@@ -209,19 +195,70 @@ function formatValue(
209
195
  }
210
196
 
211
197
  if (value.type === "number") {
212
- return formatMinMax(value.min, value.max);
198
+ switch (value.operator) {
199
+ case "between":
200
+ return {
201
+ operator: OPERATOR_LABELS.between.toLowerCase(),
202
+ value: `${value.min} - ${value.max}`,
203
+ };
204
+ case "==":
205
+ case "!=":
206
+ case ">":
207
+ case ">=":
208
+ case "<":
209
+ case "<=":
210
+ return { operator: value.operator, value: String(value.value) };
211
+ }
213
212
  }
214
- if (value.type === "date") {
215
- return formatMinMax(value.min?.toISOString(), value.max?.toISOString());
216
- }
217
- if (value.type === "time") {
218
- return formatMinMax(
219
- value.min ? timeFormatter.format(value.min) : undefined,
220
- value.max ? timeFormatter.format(value.max) : undefined,
221
- );
213
+ if (value.type === "text") {
214
+ switch (value.operator) {
215
+ case "in":
216
+ case "not_in": {
217
+ const items = value.values.map((v) => `"${v}"`);
218
+ return {
219
+ operator: value.operator === "in" ? "is in" : "not in",
220
+ value: `[${items.join(", ")}]`,
221
+ };
222
+ }
223
+ case "is_empty":
224
+ return { operator: "is empty" };
225
+ case "contains":
226
+ case "equals":
227
+ case "does_not_equal":
228
+ case "regex":
229
+ case "starts_with":
230
+ case "ends_with":
231
+ return {
232
+ operator: OPERATOR_LABELS[value.operator].toLowerCase(),
233
+ value: `"${value.text}"`,
234
+ };
235
+ }
222
236
  }
223
- if (value.type === "datetime") {
224
- return formatMinMax(value.min?.toISOString(), value.max?.toISOString());
237
+ if (
238
+ value.type === "date" ||
239
+ value.type === "datetime" ||
240
+ value.type === "time"
241
+ ) {
242
+ const format =
243
+ value.type === "time"
244
+ ? (d: Date) => timeFormatter.format(d)
245
+ : value.type === "date"
246
+ ? dateToISODate
247
+ : dateToISODateTime;
248
+ switch (value.operator) {
249
+ case "between":
250
+ return {
251
+ operator: OPERATOR_LABELS.between.toLowerCase(),
252
+ value: `${format(value.min)} - ${format(value.max)}`,
253
+ };
254
+ case "==":
255
+ case "!=":
256
+ case ">":
257
+ case ">=":
258
+ case "<":
259
+ case "<=":
260
+ return { operator: value.operator, value: format(value.value) };
261
+ }
225
262
  }
226
263
  if (value.type === "boolean") {
227
264
  return { operator: `is ${value.value ? "True" : "False"}` };
@@ -235,28 +272,6 @@ function formatValue(
235
272
  value: `[${stringifiedOptions.join(", ")}]`,
236
273
  };
237
274
  }
238
- if (value.type === "text") {
239
- return { operator: "contains", value: `"${value.text}"` };
240
- }
241
275
  logNever(value);
242
276
  return undefined;
243
277
  }
244
-
245
- function formatMinMax(
246
- min: string | number | undefined,
247
- max: string | number | undefined,
248
- ): FormattedFilter | undefined {
249
- if (min === undefined && max === undefined) {
250
- return;
251
- }
252
- if (min === max) {
253
- return { operator: "==", value: String(min) };
254
- }
255
- if (min === undefined) {
256
- return { operator: "<=", value: String(max) };
257
- }
258
- if (max === undefined) {
259
- return { operator: ">=", value: String(min) };
260
- }
261
- return { operator: "between", value: `${min} - ${max}` };
262
- }
@@ -32,33 +32,113 @@ export type FilterType =
32
32
  | "select"
33
33
  | "boolean";
34
34
 
35
+ export const NULLISH_OPS = ["is_null", "is_not_null"] as const;
36
+ export const MEMBERSHIP_OPS = ["in", "not_in"] as const;
37
+ export const NUMBER_COMPARISON_OPS = [
38
+ "==",
39
+ "!=",
40
+ ">",
41
+ ">=",
42
+ "<",
43
+ "<=",
44
+ ] as const;
45
+ export const TEXT_SCALAR_OPS = [
46
+ "contains",
47
+ "equals",
48
+ "does_not_equal",
49
+ "regex",
50
+ "starts_with",
51
+ "ends_with",
52
+ ] as const;
53
+
54
+ export const DATETIME_COMPARISON_OPS = [
55
+ "==",
56
+ "!=",
57
+ ">",
58
+ ">=",
59
+ "<",
60
+ "<=",
61
+ ] as const;
62
+
63
+ export const NUMBER_OPS = [
64
+ "between",
65
+ ...NUMBER_COMPARISON_OPS,
66
+ ...NULLISH_OPS,
67
+ ] as const;
68
+ export const TEXT_OPS = [
69
+ ...TEXT_SCALAR_OPS,
70
+ ...MEMBERSHIP_OPS,
71
+ "is_empty",
72
+ ...NULLISH_OPS,
73
+ ] as const;
74
+ export const DATETIME_OPS = [
75
+ "between",
76
+ ...DATETIME_COMPARISON_OPS,
77
+ ...NULLISH_OPS,
78
+ ] as const;
79
+
80
+ export type NullishOp = (typeof NULLISH_OPS)[number];
81
+ export type MembershipOp = (typeof MEMBERSHIP_OPS)[number];
82
+ export type NumberComparisonOp = (typeof NUMBER_COMPARISON_OPS)[number];
83
+ export type TextScalarOp = (typeof TEXT_SCALAR_OPS)[number];
84
+ export type DatetimeComparisonOp = (typeof DATETIME_COMPARISON_OPS)[number];
85
+
86
+ const makeOpGuard = <T extends OperatorType>(ops: readonly T[]) => {
87
+ const set = new Set<OperatorType>(ops);
88
+ return (op: OperatorType): op is T => set.has(op);
89
+ };
90
+
91
+ export const isNumberComparisonOp = makeOpGuard(NUMBER_COMPARISON_OPS);
92
+ export const isTextScalarOp = makeOpGuard(TEXT_SCALAR_OPS);
93
+ export const isDatetimeComparisonOp = makeOpGuard(DATETIME_COMPARISON_OPS);
94
+
95
+ interface NullishOpts {
96
+ operator: NullishOp;
97
+ }
98
+
99
+ type NumberFilterOpts =
100
+ | { operator: "between"; min: number; max: number }
101
+ | { operator: NumberComparisonOp; value: number }
102
+ | NullishOpts;
103
+
104
+ type TextFilterOpts =
105
+ | { operator: TextScalarOp; text: string }
106
+ | { operator: MembershipOp; values: string[] }
107
+ | { operator: "is_empty" }
108
+ | NullishOpts;
109
+
110
+ type DateLikeFilterOpts =
111
+ | { operator: "between"; min: Date; max: Date }
112
+ | { operator: DatetimeComparisonOp; value: Date }
113
+ | NullishOpts;
114
+
35
115
  // Filter is a factory function that creates a filter object
36
116
  export const Filter = {
37
- number(opts: { min?: number; max?: number; operator?: OperatorType }) {
117
+ number(opts: NumberFilterOpts) {
38
118
  return {
39
119
  type: "number",
40
120
  ...opts,
41
121
  } as const;
42
122
  },
43
- text(opts: { text?: string; operator: OperatorType }) {
123
+ text(opts: TextFilterOpts) {
44
124
  return {
45
125
  type: "text",
46
126
  ...opts,
47
127
  } as const;
48
128
  },
49
- date(opts: { min?: Date; max?: Date; operator?: OperatorType }) {
129
+ date(opts: DateLikeFilterOpts) {
50
130
  return {
51
131
  type: "date",
52
132
  ...opts,
53
133
  } as const;
54
134
  },
55
- datetime(opts: { min?: Date; max?: Date; operator?: OperatorType }) {
135
+ datetime(opts: DateLikeFilterOpts) {
56
136
  return {
57
137
  type: "datetime",
58
138
  ...opts,
59
139
  } as const;
60
140
  },
61
- time(opts: { min?: Date; max?: Date; operator?: OperatorType }) {
141
+ time(opts: DateLikeFilterOpts) {
62
142
  return {
63
143
  type: "time",
64
144
  ...opts,
@@ -84,6 +164,35 @@ export type ColumnFilterForType<T extends FilterType> = T extends FilterType
84
164
  ? Extract<ColumnFilterValue, { type: T }>
85
165
  : never;
86
166
 
167
+ function pad2(n: number): string {
168
+ return n.toString().padStart(2, "0");
169
+ }
170
+
171
+ function pad4(n: number): string {
172
+ return n.toString().padStart(4, "0");
173
+ }
174
+
175
+ export function dateToISODate(d: Date): string {
176
+ return `${pad4(d.getFullYear())}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
177
+ }
178
+
179
+ export function dateToISOTime(d: Date): string {
180
+ return `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
181
+ }
182
+
183
+ export function dateToISODateTime(d: Date): string {
184
+ return `${dateToISODate(d)}T${dateToISOTime(d)}`;
185
+ }
186
+
187
+ function isNullishFilter(
188
+ filter: ColumnFilterValue,
189
+ ): filter is Extract<
190
+ ColumnFilterValue,
191
+ { operator: "is_null" | "is_not_null" }
192
+ > {
193
+ return filter.operator === "is_null" || filter.operator === "is_not_null";
194
+ }
195
+
87
196
  export function filterToFilterCondition(
88
197
  columnIdString: string,
89
198
  filter: ColumnFilterValue | undefined,
@@ -93,12 +202,11 @@ export function filterToFilterCondition(
93
202
  }
94
203
  const columnId = columnIdString as ColumnId;
95
204
 
96
- if (filter.operator === "is_null" || filter.operator === "is_not_null") {
205
+ if (isNullishFilter(filter)) {
97
206
  return [
98
207
  {
99
208
  column_id: columnId,
100
209
  operator: filter.operator,
101
- value: undefined,
102
210
  type: "condition",
103
211
  negate: false,
104
212
  },
@@ -106,103 +214,114 @@ export function filterToFilterCondition(
106
214
  }
107
215
 
108
216
  switch (filter.type) {
109
- case "number": {
110
- const conditions: FilterConditionType[] = [];
111
- if (filter.min !== undefined) {
112
- conditions.push({
113
- column_id: columnId,
114
- operator: ">=",
115
- value: filter.min,
116
- type: "condition",
117
- negate: false,
118
- });
217
+ case "number":
218
+ switch (filter.operator) {
219
+ case "between":
220
+ return [
221
+ {
222
+ column_id: columnId,
223
+ operator: "between",
224
+ value: { min: filter.min, max: filter.max },
225
+ type: "condition",
226
+ negate: false,
227
+ },
228
+ ];
229
+ case "==":
230
+ case "!=":
231
+ case ">":
232
+ case ">=":
233
+ case "<":
234
+ case "<=":
235
+ return [
236
+ {
237
+ column_id: columnId,
238
+ operator: filter.operator,
239
+ value: filter.value,
240
+ type: "condition",
241
+ negate: false,
242
+ },
243
+ ];
244
+ default:
245
+ assertNever(filter);
119
246
  }
120
- if (filter.max !== undefined) {
121
- conditions.push({
122
- column_id: columnId,
123
- operator: "<=",
124
- value: filter.max,
125
- type: "condition",
126
- negate: false,
127
- });
128
- }
129
- return conditions;
130
- }
131
247
  case "text":
132
- return [
133
- {
134
- column_id: columnId,
135
- operator: filter.operator,
136
- value: filter.text,
137
- type: "condition",
138
- negate: false,
139
- },
140
- ];
141
- case "datetime": {
142
- const conditions: FilterConditionType[] = [];
143
- if (filter.min !== undefined) {
144
- conditions.push({
145
- column_id: columnId,
146
- operator: ">=",
147
- value: filter.min.toISOString(),
148
- type: "condition",
149
- negate: false,
150
- });
151
- }
152
- if (filter.max !== undefined) {
153
- conditions.push({
154
- column_id: columnId,
155
- operator: "<=",
156
- value: filter.max.toISOString(),
157
- type: "condition",
158
- negate: false,
159
- });
160
- }
161
- return conditions;
162
- }
163
- case "date": {
164
- const conditions: FilterConditionType[] = [];
165
- if (filter.min !== undefined) {
166
- conditions.push({
167
- column_id: columnId,
168
- operator: ">=",
169
- value: filter.min.toISOString(),
170
- type: "condition",
171
- negate: false,
172
- });
248
+ switch (filter.operator) {
249
+ case "contains":
250
+ case "equals":
251
+ case "does_not_equal":
252
+ case "regex":
253
+ case "starts_with":
254
+ case "ends_with":
255
+ return [
256
+ {
257
+ column_id: columnId,
258
+ operator: filter.operator,
259
+ value: filter.text,
260
+ type: "condition",
261
+ negate: false,
262
+ },
263
+ ];
264
+ case "in":
265
+ case "not_in":
266
+ return [
267
+ {
268
+ column_id: columnId,
269
+ operator: filter.operator,
270
+ value: filter.values,
271
+ type: "condition",
272
+ negate: false,
273
+ },
274
+ ];
275
+ case "is_empty":
276
+ return [
277
+ {
278
+ column_id: columnId,
279
+ operator: "is_empty",
280
+ type: "condition",
281
+ negate: false,
282
+ },
283
+ ];
284
+ default:
285
+ assertNever(filter);
173
286
  }
174
- if (filter.max !== undefined) {
175
- conditions.push({
176
- column_id: columnId,
177
- operator: "<=",
178
- value: filter.max.toISOString(),
179
- type: "condition",
180
- negate: false,
181
- });
182
- }
183
- return conditions;
184
- }
287
+ case "date":
288
+ case "datetime":
185
289
  case "time": {
186
- const conditions: FilterConditionType[] = [];
187
- if (filter.min !== undefined) {
188
- conditions.push({
189
- column_id: columnId,
190
- operator: ">=",
191
- value: filter.min.toISOString(),
192
- type: "condition",
193
- negate: false,
194
- });
195
- }
196
- if (filter.max !== undefined) {
197
- conditions.push({
198
- column_id: columnId,
199
- operator: "<=",
200
- value: filter.max.toISOString(),
201
- type: "condition",
202
- negate: false,
203
- });
290
+ const encode =
291
+ filter.type === "date"
292
+ ? dateToISODate
293
+ : filter.type === "time"
294
+ ? dateToISOTime
295
+ : dateToISODateTime;
296
+ switch (filter.operator) {
297
+ case "between":
298
+ return [
299
+ {
300
+ column_id: columnId,
301
+ operator: "between",
302
+ value: { min: encode(filter.min), max: encode(filter.max) },
303
+ type: "condition",
304
+ negate: false,
305
+ },
306
+ ];
307
+ case "==":
308
+ case "!=":
309
+ case ">":
310
+ case ">=":
311
+ case "<":
312
+ case "<=":
313
+ return [
314
+ {
315
+ column_id: columnId,
316
+ operator: filter.operator,
317
+ value: encode(filter.value),
318
+ type: "condition",
319
+ negate: false,
320
+ },
321
+ ];
322
+ default:
323
+ assertNever(filter);
204
324
  }
205
- return conditions;
206
325
  }
207
326
  case "boolean":
208
327
  if (filter.value) {
@@ -210,7 +329,6 @@ export function filterToFilterCondition(
210
329
  {
211
330
  column_id: columnId,
212
331
  operator: "is_true",
213
- value: undefined,
214
332
  type: "condition",
215
333
  negate: false,
216
334
  },
@@ -221,7 +339,6 @@ export function filterToFilterCondition(
221
339
  {
222
340
  column_id: columnId,
223
341
  operator: "is_false",
224
- value: undefined,
225
342
  type: "condition",
226
343
  negate: false,
227
344
  },
@@ -328,14 +328,21 @@ export const FilterButtons = ({
328
328
  onApply,
329
329
  onClear,
330
330
  clearButtonDisabled,
331
+ applyButtonDisabled,
331
332
  }: {
332
333
  onApply: () => void;
333
334
  onClear: () => void;
334
335
  clearButtonDisabled?: boolean;
336
+ applyButtonDisabled?: boolean;
335
337
  }) => {
336
338
  return (
337
339
  <div className="flex gap-2 px-2 justify-between">
338
- <Button variant="link" size="sm" onClick={onApply}>
340
+ <Button
341
+ variant="link"
342
+ size="sm"
343
+ onClick={onApply}
344
+ disabled={applyButtonDisabled}
345
+ >
339
346
  Apply
340
347
  </Button>
341
348
  <Button
@@ -0,0 +1,25 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import type { OperatorType } from "@/plugins/impl/data-frames/utils/operators";
3
+
4
+ export const OPERATOR_LABELS: Record<OperatorType | "between", string> = {
5
+ "==": "Equals",
6
+ "!=": "Doesn't equal",
7
+ ">": "Greater than",
8
+ ">=": "Greater than or equal",
9
+ "<": "Less than",
10
+ "<=": "Less than or equal",
11
+ between: "Between",
12
+ contains: "Contains",
13
+ equals: "Equals",
14
+ does_not_equal: "Doesn't equal",
15
+ starts_with: "Starts with",
16
+ ends_with: "Ends with",
17
+ regex: "Matches regex",
18
+ in: "Is in",
19
+ not_in: "Not in",
20
+ is_empty: "Is empty",
21
+ is_true: "Is true",
22
+ is_false: "Is false",
23
+ is_null: "Is null",
24
+ is_not_null: "Is not null",
25
+ };
@@ -0,0 +1,61 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import React from "react";
3
+ import { cn } from "@/utils/cn";
4
+ import { Input } from "../ui/input";
5
+
6
+ export interface RegexInputProps {
7
+ id?: string;
8
+ value: string;
9
+ onChange: (next: string) => void;
10
+ onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
11
+ placeholder?: string;
12
+ className?: string;
13
+ autoFocus?: boolean;
14
+ "aria-label"?: string;
15
+ }
16
+
17
+ export const RegexInput = React.forwardRef<HTMLInputElement, RegexInputProps>(
18
+ (
19
+ {
20
+ id,
21
+ value,
22
+ onChange,
23
+ onKeyDown,
24
+ placeholder = "pattern",
25
+ className,
26
+ autoFocus,
27
+ "aria-label": ariaLabel,
28
+ },
29
+ ref,
30
+ ) => (
31
+ <div
32
+ className={cn(
33
+ "flex items-stretch h-6 mb-1 rounded-sm border border-input bg-background shadow-xs-solid focus-within:shadow-md-solid focus-within:ring-1 focus-within:ring-ring focus-within:border-primary",
34
+ className,
35
+ )}
36
+ >
37
+ <Slash />
38
+ <Input
39
+ ref={ref}
40
+ id={id}
41
+ type="text"
42
+ value={value}
43
+ onChange={(e) => onChange(e.target.value)}
44
+ onKeyDown={onKeyDown}
45
+ placeholder={placeholder}
46
+ autoFocus={autoFocus}
47
+ aria-label={ariaLabel}
48
+ rootClassName="flex-1 min-w-0"
49
+ className="border-0 mb-0 h-full shadow-none! hover:shadow-none! focus-visible:shadow-none! focus-visible:ring-0 focus-visible:border-0 rounded-none bg-transparent"
50
+ />
51
+ <Slash />
52
+ </div>
53
+ ),
54
+ );
55
+ RegexInput.displayName = "RegexInput";
56
+
57
+ const Slash = () => (
58
+ <span className="px-1.5 flex items-center text-muted-foreground font-code text-sm select-none">
59
+ /
60
+ </span>
61
+ );