@marimo-team/islands 0.23.4-dev0 → 0.23.4-dev5

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.
@@ -1,24 +1,31 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  "use no memo";
3
3
 
4
- import type {
5
- ColumnFilter,
6
- ColumnFiltersState,
7
- Table,
8
- } from "@tanstack/react-table";
4
+ import type { ColumnFiltersState, Table } from "@tanstack/react-table";
9
5
  import { XIcon } from "lucide-react";
6
+ import { useState } from "react";
10
7
  import { type DateFormatter, useDateFormatter } from "react-aria";
8
+ import type { CalculateTopKRows } from "@/plugins/impl/DataTablePlugin";
11
9
  import { logNever } from "@/utils/assertNever";
10
+ import { cn } from "@/utils/cn";
12
11
  import { Badge } from "../ui/badge";
12
+ import { Button } from "../ui/button";
13
+ import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
14
+ import { FilterPillEditor } from "./filter-pill-editor";
13
15
  import type { ColumnFilterValue } from "./filters";
14
16
  import { stringifyUnknownValue } from "./utils";
15
17
 
16
18
  interface Props<TData> {
17
19
  filters: ColumnFiltersState | undefined;
18
20
  table: Table<TData>;
21
+ calculateTopKRows?: CalculateTopKRows;
19
22
  }
20
23
 
21
- export const FilterPills = <TData,>({ filters, table }: Props<TData>) => {
24
+ export const FilterPills = <TData,>({
25
+ filters,
26
+ table,
27
+ calculateTopKRows,
28
+ }: Props<TData>) => {
22
29
  const timeFormatter = useDateFormatter({
23
30
  hour: "2-digit",
24
31
  minute: "2-digit",
@@ -29,49 +36,176 @@ export const FilterPills = <TData,>({ filters, table }: Props<TData>) => {
29
36
  return null;
30
37
  }
31
38
 
32
- function renderFilterPill(filter: ColumnFilter) {
33
- const formattedValue = formatValue(
34
- filter.value as ColumnFilterValue,
35
- timeFormatter,
36
- );
37
- if (!formattedValue) {
38
- return null;
39
- }
40
-
41
- return (
42
- <Badge key={filter.id} variant="secondary" className="dark:invert">
43
- {filter.id} {formattedValue}{" "}
44
- <span
45
- className="cursor-pointer opacity-60 hover:opacity-100 pl-1 py-[2px]"
46
- onClick={() => {
39
+ return (
40
+ <div part="filter-pills" className="flex flex-wrap gap-2 px-1 py-2">
41
+ {filters.map((filter) => (
42
+ <FilterPill
43
+ key={filter.id}
44
+ columnId={filter.id}
45
+ value={filter.value as ColumnFilterValue}
46
+ timeFormatter={timeFormatter}
47
+ table={table}
48
+ calculateTopKRows={calculateTopKRows}
49
+ onRemove={() =>
47
50
  table.setColumnFilters((filters) =>
48
51
  filters.filter((f) => f.id !== filter.id),
49
- );
50
- }}
51
- >
52
- <XIcon className="w-3.5 h-3.5" />
53
- </span>
52
+ )
53
+ }
54
+ />
55
+ ))}
56
+ </div>
57
+ );
58
+ };
59
+
60
+ interface FilterPillProps<TData> {
61
+ columnId: string;
62
+ value: ColumnFilterValue;
63
+ timeFormatter: DateFormatter;
64
+ table: Table<TData>;
65
+ calculateTopKRows?: CalculateTopKRows;
66
+ onRemove: () => void;
67
+ }
68
+
69
+ const FilterPill = <TData,>({
70
+ columnId,
71
+ value,
72
+ timeFormatter,
73
+ table,
74
+ calculateTopKRows,
75
+ onRemove,
76
+ }: FilterPillProps<TData>) => {
77
+ const [open, setOpen] = useState(false);
78
+
79
+ const formatted = formatValue(value, timeFormatter);
80
+ if (!formatted) {
81
+ return null;
82
+ }
83
+
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
+ const twoSegment = formatted.value === undefined;
92
+
93
+ const handleRemove = (e: React.MouseEvent) => {
94
+ e.stopPropagation();
95
+ onRemove();
96
+ };
97
+
98
+ const segments = (
99
+ <>
100
+ <span className="text-foreground">{columnId}</span>
101
+ <Separator />
102
+ <span
103
+ className={cn(
104
+ "font-normal",
105
+ twoSegment ? "text-foreground" : "text-foreground/70",
106
+ )}
107
+ >
108
+ {formatted.operator}
109
+ </span>
110
+ {!twoSegment && (
111
+ <>
112
+ <Separator />
113
+ <span className="text-foreground">{formatted.value}</span>
114
+ </>
115
+ )}
116
+ </>
117
+ );
118
+
119
+ const removeButton = (
120
+ <Button
121
+ type="button"
122
+ size="icon"
123
+ variant="ghost"
124
+ className="ml-1 rounded-full text-destructive/60 hover:text-destructive hover:shadow-none hover:bg-transparent"
125
+ onClick={handleRemove}
126
+ aria-label="Remove filter"
127
+ >
128
+ <XIcon className="h-3.5 w-3.5" aria-hidden={true} />
129
+ </Button>
130
+ );
131
+
132
+ if (isReadOnly) {
133
+ return (
134
+ <Badge
135
+ variant="outline"
136
+ className="bg-background border-border text-foreground"
137
+ >
138
+ {segments}
139
+ {removeButton}
54
140
  </Badge>
55
141
  );
56
142
  }
57
143
 
58
144
  return (
59
- <div className="flex flex-wrap gap-2 px-1">
60
- {filters.map(renderFilterPill)}
61
- </div>
145
+ <Popover open={open} onOpenChange={setOpen} modal={true}>
146
+ <Badge
147
+ variant="outline"
148
+ className={cn(
149
+ "bg-background border-border text-foreground",
150
+ "hover:bg-accent hover:text-accent-foreground",
151
+ "has-data-[state=open]:bg-accent has-data-[state=open]:text-accent-foreground",
152
+ "transition-colors",
153
+ )}
154
+ >
155
+ <PopoverTrigger asChild={true}>
156
+ <button
157
+ type="button"
158
+ className="inline-flex items-center cursor-pointer bg-transparent border-0 p-0 [font:inherit] text-inherit"
159
+ aria-label={`Edit filter on ${columnId}`}
160
+ >
161
+ {segments}
162
+ </button>
163
+ </PopoverTrigger>
164
+ {removeButton}
165
+ </Badge>
166
+ <PopoverContent
167
+ className="w-auto p-0"
168
+ align="start"
169
+ alignOffset={-10}
170
+ sideOffset={10}
171
+ avoidCollisions={true}
172
+ onOpenAutoFocus={(e) => e.preventDefault()}
173
+ >
174
+ <FilterPillEditor
175
+ snapshot={{ columnId, value }}
176
+ table={table}
177
+ calculateTopKRows={calculateTopKRows}
178
+ onClose={() => setOpen(false)}
179
+ />
180
+ </PopoverContent>
181
+ </Popover>
62
182
  );
63
183
  };
64
184
 
65
- function formatValue(value: ColumnFilterValue, timeFormatter: DateFormatter) {
185
+ function Separator() {
186
+ return (
187
+ <span aria-hidden={true} className="mx-1.5 w-px h-3 bg-foreground/30" />
188
+ );
189
+ }
190
+
191
+ interface FormattedFilter {
192
+ operator: string;
193
+ value?: string;
194
+ }
195
+
196
+ function formatValue(
197
+ value: ColumnFilterValue,
198
+ timeFormatter: DateFormatter,
199
+ ): FormattedFilter | undefined {
66
200
  if (!("type" in value)) {
67
201
  return;
68
202
  }
69
203
 
70
204
  if (value.operator === "is_null") {
71
- return "is null";
205
+ return { operator: "is null" };
72
206
  }
73
207
  if (value.operator === "is_not_null") {
74
- return "is not null";
208
+ return { operator: "is not null" };
75
209
  }
76
210
 
77
211
  if (value.type === "number") {
@@ -90,17 +224,19 @@ function formatValue(value: ColumnFilterValue, timeFormatter: DateFormatter) {
90
224
  return formatMinMax(value.min?.toISOString(), value.max?.toISOString());
91
225
  }
92
226
  if (value.type === "boolean") {
93
- return `is ${value.value ? "True" : "False"}`;
227
+ return { operator: `is ${value.value ? "True" : "False"}` };
94
228
  }
95
229
  if (value.type === "select") {
96
230
  const stringifiedOptions = value.options.map((o) =>
97
231
  stringifyUnknownValue({ value: o }),
98
232
  );
99
- const operator = value.operator === "in" ? "is in" : "not in";
100
- return `${operator} [${stringifiedOptions.join(", ")}]`;
233
+ return {
234
+ operator: value.operator === "in" ? "is in" : "not in",
235
+ value: `[${stringifiedOptions.join(", ")}]`,
236
+ };
101
237
  }
102
238
  if (value.type === "text") {
103
- return `contains "${value.text}"`;
239
+ return { operator: "contains", value: `"${value.text}"` };
104
240
  }
105
241
  logNever(value);
106
242
  return undefined;
@@ -109,18 +245,18 @@ function formatValue(value: ColumnFilterValue, timeFormatter: DateFormatter) {
109
245
  function formatMinMax(
110
246
  min: string | number | undefined,
111
247
  max: string | number | undefined,
112
- ) {
248
+ ): FormattedFilter | undefined {
113
249
  if (min === undefined && max === undefined) {
114
250
  return;
115
251
  }
116
252
  if (min === max) {
117
- return `== ${min}`;
253
+ return { operator: "==", value: String(min) };
118
254
  }
119
255
  if (min === undefined) {
120
- return `<= ${max}`;
256
+ return { operator: "<=", value: String(max) };
121
257
  }
122
258
  if (max === undefined) {
123
- return `>= ${min}`;
259
+ return { operator: ">=", value: String(min) };
124
260
  }
125
- return `${min} - ${max}`;
261
+ return { operator: "between", value: `${min} - ${max}` };
126
262
  }
@@ -1,6 +1,6 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { Check } from "lucide-react";
3
+ import { Check, Minus } from "lucide-react";
4
4
  import { Checkbox as CheckboxPrimitive } from "radix-ui";
5
5
  import * as React from "react";
6
6
 
@@ -13,18 +13,22 @@ const Checkbox = React.forwardRef<
13
13
  <CheckboxPrimitive.Root
14
14
  ref={ref}
15
15
  className={cn(
16
- "flex items-center justify-center peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
16
+ "flex items-center justify-center peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground",
17
17
  className,
18
18
  )}
19
19
  {...props}
20
20
  >
21
21
  <CheckboxPrimitive.Indicator
22
- // `max-h-[14px] overflow-hidden` are for the checkmark icon to not resize the checkbox
22
+ // `max-h-[14px] overflow-hidden` are for the icon to not resize the checkbox
23
23
  className={cn(
24
24
  "flex items-center justify-center text-current max-h-[14px] overflow-hidden",
25
25
  )}
26
26
  >
27
- <Check className="h-4 w-4" />
27
+ {props.checked === "indeterminate" ? (
28
+ <Minus className="h-4 w-4" />
29
+ ) : (
30
+ <Check className="h-4 w-4" />
31
+ )}
28
32
  </CheckboxPrimitive.Indicator>
29
33
  </CheckboxPrimitive.Root>
30
34
  ));
@@ -37,6 +37,7 @@ interface ComboboxCommonProps<TValue> {
37
37
  onSearchChange?: (search: string) => void;
38
38
  emptyState?: React.ReactNode;
39
39
  className?: string;
40
+ id?: string;
40
41
  keepPopoverOpenOnSelect?: boolean;
41
42
  }
42
43
 
@@ -93,6 +94,7 @@ export const Combobox = <TValue,>({
93
94
  chips = false,
94
95
  chipsClassName,
95
96
  keepPopoverOpenOnSelect,
97
+ id,
96
98
  ...rest
97
99
  }: ComboboxProps<TValue>) => {
98
100
  const [open = false, setOpen] = useControllableState({
@@ -169,6 +171,7 @@ export const Combobox = <TValue,>({
169
171
  <Popover open={open} onOpenChange={setOpen}>
170
172
  <PopoverTrigger asChild={true}>
171
173
  <div
174
+ id={id}
172
175
  className={cn(
173
176
  "flex h-6 w-fit mb-1 shadow-xs-solid items-center justify-between rounded-sm border border-input bg-transparent px-2 text-sm font-prose ring-offset-background placeholder:text-muted-foreground hover:shadow-sm-solid focus:outline-hidden focus:ring-1 focus:ring-ring focus:border-primary focus:shadow-md-solid disabled:cursor-not-allowed disabled:opacity-50",
174
177
  className,
@@ -124,6 +124,10 @@
124
124
  marimo-table::part(table-footer) {
125
125
  padding-inline: 0.25rem;
126
126
  }
127
+
128
+ marimo-table::part(filter-pills) {
129
+ padding-bottom: 0;
130
+ }
127
131
  }
128
132
 
129
133
  & > :first-child {
@@ -1073,6 +1073,7 @@ const DataTableComponent = ({
1073
1073
  showFilters={showFilters}
1074
1074
  filters={filters}
1075
1075
  onFiltersChange={setFilters}
1076
+ calculateTopKRows={calculate_top_k_rows}
1076
1077
  reloading={reloading}
1077
1078
  onRowSelectionChange={handleRowSelectionChange}
1078
1079
  freezeColumnsLeft={freezeColumnsLeft}
@@ -26,7 +26,6 @@ exports[`renderZodSchema > should render a form aggregate 1`] = `
26
26
  aria-describedby="_r_26_-form-item-description"
27
27
  aria-invalid="false"
28
28
  class="relative"
29
- id="_r_26_-form-item"
30
29
  >
31
30
  <div
32
31
  aria-controls="radix-_r_27_"
@@ -34,6 +33,7 @@ exports[`renderZodSchema > should render a form aggregate 1`] = `
34
33
  aria-haspopup="dialog"
35
34
  class="flex h-6 w-fit mb-1 shadow-xs-solid items-center justify-between rounded-sm border border-input bg-transparent px-2 text-sm font-prose ring-offset-background placeholder:text-muted-foreground hover:shadow-sm-solid focus:outline-hidden focus:ring-1 focus:ring-ring focus:border-primary focus:shadow-md-solid disabled:cursor-not-allowed disabled:opacity-50 min-w-[180px]"
36
35
  data-state="closed"
36
+ id="_r_26_-form-item"
37
37
  type="button"
38
38
  >
39
39
  <span
@@ -81,7 +81,6 @@ exports[`renderZodSchema > should render a form aggregate 1`] = `
81
81
  aria-describedby="_r_28_-form-item-description"
82
82
  aria-invalid="false"
83
83
  class="relative"
84
- id="_r_28_-form-item"
85
84
  >
86
85
  <div
87
86
  aria-controls="radix-_r_29_"
@@ -89,6 +88,7 @@ exports[`renderZodSchema > should render a form aggregate 1`] = `
89
88
  aria-haspopup="dialog"
90
89
  class="flex h-6 w-fit mb-1 shadow-xs-solid items-center justify-between rounded-sm border border-input bg-transparent px-2 text-sm font-prose ring-offset-background placeholder:text-muted-foreground hover:shadow-sm-solid focus:outline-hidden focus:ring-1 focus:ring-ring focus:border-primary focus:shadow-md-solid disabled:cursor-not-allowed disabled:opacity-50 min-w-[180px]"
91
90
  data-state="closed"
91
+ id="_r_28_-form-item"
92
92
  type="button"
93
93
  >
94
94
  <span
@@ -398,7 +398,6 @@ exports[`renderZodSchema > should render a form explode_columns 1`] = `
398
398
  aria-describedby="_r_2o_-form-item-description"
399
399
  aria-invalid="false"
400
400
  class="relative"
401
- id="_r_2o_-form-item"
402
401
  >
403
402
  <div
404
403
  aria-controls="radix-_r_2p_"
@@ -406,6 +405,7 @@ exports[`renderZodSchema > should render a form explode_columns 1`] = `
406
405
  aria-haspopup="dialog"
407
406
  class="flex h-6 w-fit mb-1 shadow-xs-solid items-center justify-between rounded-sm border border-input bg-transparent px-2 text-sm font-prose ring-offset-background placeholder:text-muted-foreground hover:shadow-sm-solid focus:outline-hidden focus:ring-1 focus:ring-ring focus:border-primary focus:shadow-md-solid disabled:cursor-not-allowed disabled:opacity-50 min-w-[180px]"
408
407
  data-state="closed"
408
+ id="_r_2o_-form-item"
409
409
  type="button"
410
410
  >
411
411
  <span
@@ -595,7 +595,6 @@ exports[`renderZodSchema > should render a form group_by 1`] = `
595
595
  aria-describedby="_r_1o_-form-item-description"
596
596
  aria-invalid="false"
597
597
  class="relative"
598
- id="_r_1o_-form-item"
599
598
  >
600
599
  <div
601
600
  aria-controls="radix-_r_1p_"
@@ -603,6 +602,7 @@ exports[`renderZodSchema > should render a form group_by 1`] = `
603
602
  aria-haspopup="dialog"
604
603
  class="flex h-6 w-fit mb-1 shadow-xs-solid items-center justify-between rounded-sm border border-input bg-transparent px-2 text-sm font-prose ring-offset-background placeholder:text-muted-foreground hover:shadow-sm-solid focus:outline-hidden focus:ring-1 focus:ring-ring focus:border-primary focus:shadow-md-solid disabled:cursor-not-allowed disabled:opacity-50 min-w-[180px]"
605
604
  data-state="closed"
605
+ id="_r_1o_-form-item"
606
606
  type="button"
607
607
  >
608
608
  <span
@@ -650,7 +650,6 @@ exports[`renderZodSchema > should render a form group_by 1`] = `
650
650
  aria-describedby="_r_1q_-form-item-description"
651
651
  aria-invalid="false"
652
652
  class="relative"
653
- id="_r_1q_-form-item"
654
653
  >
655
654
  <div
656
655
  aria-controls="radix-_r_1r_"
@@ -658,6 +657,7 @@ exports[`renderZodSchema > should render a form group_by 1`] = `
658
657
  aria-haspopup="dialog"
659
658
  class="flex h-6 w-fit mb-1 shadow-xs-solid items-center justify-between rounded-sm border border-input bg-transparent px-2 text-sm font-prose ring-offset-background placeholder:text-muted-foreground hover:shadow-sm-solid focus:outline-hidden focus:ring-1 focus:ring-ring focus:border-primary focus:shadow-md-solid disabled:cursor-not-allowed disabled:opacity-50 min-w-[180px]"
660
659
  data-state="closed"
660
+ id="_r_1q_-form-item"
661
661
  type="button"
662
662
  >
663
663
  <span
@@ -756,7 +756,7 @@ exports[`renderZodSchema > should render a form group_by 1`] = `
756
756
  aria-checked="false"
757
757
  aria-describedby="_r_1u_-form-item-description"
758
758
  aria-invalid="false"
759
- class="flex items-center justify-center peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground"
759
+ class="flex items-center justify-center peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground"
760
760
  data-state="unchecked"
761
761
  data-testid="marimo-plugin-data-frames-boolean-checkbox"
762
762
  id="_r_1u_-form-item"
@@ -797,7 +797,6 @@ exports[`renderZodSchema > should render a form pivot 1`] = `
797
797
  aria-describedby="_r_3a_-form-item-description"
798
798
  aria-invalid="false"
799
799
  class="relative"
800
- id="_r_3a_-form-item"
801
800
  >
802
801
  <div
803
802
  aria-controls="radix-_r_3b_"
@@ -805,6 +804,7 @@ exports[`renderZodSchema > should render a form pivot 1`] = `
805
804
  aria-haspopup="dialog"
806
805
  class="flex h-6 w-fit mb-1 shadow-xs-solid items-center justify-between rounded-sm border border-input bg-transparent px-2 text-sm font-prose ring-offset-background placeholder:text-muted-foreground hover:shadow-sm-solid focus:outline-hidden focus:ring-1 focus:ring-ring focus:border-primary focus:shadow-md-solid disabled:cursor-not-allowed disabled:opacity-50 min-w-[180px]"
807
806
  data-state="closed"
807
+ id="_r_3a_-form-item"
808
808
  type="button"
809
809
  >
810
810
  <span
@@ -852,7 +852,6 @@ exports[`renderZodSchema > should render a form pivot 1`] = `
852
852
  aria-describedby="_r_3c_-form-item-description"
853
853
  aria-invalid="false"
854
854
  class="relative"
855
- id="_r_3c_-form-item"
856
855
  >
857
856
  <div
858
857
  aria-controls="radix-_r_3d_"
@@ -860,6 +859,7 @@ exports[`renderZodSchema > should render a form pivot 1`] = `
860
859
  aria-haspopup="dialog"
861
860
  class="flex h-6 w-fit mb-1 shadow-xs-solid items-center justify-between rounded-sm border border-input bg-transparent px-2 text-sm font-prose ring-offset-background placeholder:text-muted-foreground hover:shadow-sm-solid focus:outline-hidden focus:ring-1 focus:ring-ring focus:border-primary focus:shadow-md-solid disabled:cursor-not-allowed disabled:opacity-50 min-w-[180px]"
862
861
  data-state="closed"
862
+ id="_r_3c_-form-item"
863
863
  type="button"
864
864
  >
865
865
  <span
@@ -907,7 +907,6 @@ exports[`renderZodSchema > should render a form pivot 1`] = `
907
907
  aria-describedby="_r_3e_-form-item-description"
908
908
  aria-invalid="false"
909
909
  class="relative"
910
- id="_r_3e_-form-item"
911
910
  >
912
911
  <div
913
912
  aria-controls="radix-_r_3f_"
@@ -915,6 +914,7 @@ exports[`renderZodSchema > should render a form pivot 1`] = `
915
914
  aria-haspopup="dialog"
916
915
  class="flex h-6 w-fit mb-1 shadow-xs-solid items-center justify-between rounded-sm border border-input bg-transparent px-2 text-sm font-prose ring-offset-background placeholder:text-muted-foreground hover:shadow-sm-solid focus:outline-hidden focus:ring-1 focus:ring-ring focus:border-primary focus:shadow-md-solid disabled:cursor-not-allowed disabled:opacity-50 min-w-[180px]"
917
916
  data-state="closed"
917
+ id="_r_3e_-form-item"
918
918
  type="button"
919
919
  >
920
920
  <span
@@ -1256,7 +1256,7 @@ exports[`renderZodSchema > should render a form sample_rows 1`] = `
1256
1256
  aria-checked="false"
1257
1257
  aria-describedby="_r_2n_-form-item-description"
1258
1258
  aria-invalid="false"
1259
- class="flex items-center justify-center peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground"
1259
+ class="flex items-center justify-center peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground"
1260
1260
  data-state="unchecked"
1261
1261
  data-testid="marimo-plugin-data-frames-boolean-checkbox"
1262
1262
  id="_r_2n_-form-item"
@@ -1297,7 +1297,6 @@ exports[`renderZodSchema > should render a form select_columns 1`] = `
1297
1297
  aria-describedby="_r_3_-form-item-description"
1298
1298
  aria-invalid="false"
1299
1299
  class="relative"
1300
- id="_r_3_-form-item"
1301
1300
  >
1302
1301
  <div
1303
1302
  aria-controls="radix-_r_4_"
@@ -1305,6 +1304,7 @@ exports[`renderZodSchema > should render a form select_columns 1`] = `
1305
1304
  aria-haspopup="dialog"
1306
1305
  class="flex h-6 w-fit mb-1 shadow-xs-solid items-center justify-between rounded-sm border border-input bg-transparent px-2 text-sm font-prose ring-offset-background placeholder:text-muted-foreground hover:shadow-sm-solid focus:outline-hidden focus:ring-1 focus:ring-ring focus:border-primary focus:shadow-md-solid disabled:cursor-not-allowed disabled:opacity-50 min-w-[180px]"
1307
1306
  data-state="closed"
1307
+ id="_r_3_-form-item"
1308
1308
  type="button"
1309
1309
  >
1310
1310
  <span
@@ -1471,7 +1471,7 @@ exports[`renderZodSchema > should render a form sort_column 1`] = `
1471
1471
  aria-checked="false"
1472
1472
  aria-describedby="_r_1f_-form-item-description"
1473
1473
  aria-invalid="false"
1474
- class="flex items-center justify-center peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground"
1474
+ class="flex items-center justify-center peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground"
1475
1475
  data-state="unchecked"
1476
1476
  data-testid="marimo-plugin-data-frames-boolean-checkbox"
1477
1477
  id="_r_1f_-form-item"
@@ -1580,7 +1580,6 @@ exports[`renderZodSchema > should render a form unique 1`] = `
1580
1580
  aria-describedby="_r_31_-form-item-description"
1581
1581
  aria-invalid="false"
1582
1582
  class="relative"
1583
- id="_r_31_-form-item"
1584
1583
  >
1585
1584
  <div
1586
1585
  aria-controls="radix-_r_32_"
@@ -1588,6 +1587,7 @@ exports[`renderZodSchema > should render a form unique 1`] = `
1588
1587
  aria-haspopup="dialog"
1589
1588
  class="flex h-6 w-fit mb-1 shadow-xs-solid items-center justify-between rounded-sm border border-input bg-transparent px-2 text-sm font-prose ring-offset-background placeholder:text-muted-foreground hover:shadow-sm-solid focus:outline-hidden focus:ring-1 focus:ring-ring focus:border-primary focus:shadow-md-solid disabled:cursor-not-allowed disabled:opacity-50 min-w-[180px]"
1590
1589
  data-state="closed"
1590
+ id="_r_31_-form-item"
1591
1591
  type="button"
1592
1592
  >
1593
1593
  <span
@@ -1739,7 +1739,6 @@ exports[`renders custom forms column_id_array 1`] = `
1739
1739
  aria-describedby="_r_40_-form-item-description"
1740
1740
  aria-invalid="false"
1741
1741
  class="relative"
1742
- id="_r_40_-form-item"
1743
1742
  >
1744
1743
  <div
1745
1744
  aria-controls="radix-_r_41_"
@@ -1747,6 +1746,7 @@ exports[`renders custom forms column_id_array 1`] = `
1747
1746
  aria-haspopup="dialog"
1748
1747
  class="flex h-6 w-fit mb-1 shadow-xs-solid items-center justify-between rounded-sm border border-input bg-transparent px-2 text-sm font-prose ring-offset-background placeholder:text-muted-foreground hover:shadow-sm-solid focus:outline-hidden focus:ring-1 focus:ring-ring focus:border-primary focus:shadow-md-solid disabled:cursor-not-allowed disabled:opacity-50 min-w-[180px]"
1749
1748
  data-state="closed"
1749
+ id="_r_40_-form-item"
1750
1750
  type="button"
1751
1751
  >
1752
1752
  <span
@@ -1789,7 +1789,6 @@ exports[`renders custom forms column_id_dot_array 1`] = `
1789
1789
  aria-describedby="_r_42_-form-item-description"
1790
1790
  aria-invalid="false"
1791
1791
  class="relative"
1792
- id="_r_42_-form-item"
1793
1792
  >
1794
1793
  <div
1795
1794
  aria-controls="radix-_r_43_"
@@ -1797,6 +1796,7 @@ exports[`renders custom forms column_id_dot_array 1`] = `
1797
1796
  aria-haspopup="dialog"
1798
1797
  class="flex h-6 w-fit mb-1 shadow-xs-solid items-center justify-between rounded-sm border border-input bg-transparent px-2 text-sm font-prose ring-offset-background placeholder:text-muted-foreground hover:shadow-sm-solid focus:outline-hidden focus:ring-1 focus:ring-ring focus:border-primary focus:shadow-md-solid disabled:cursor-not-allowed disabled:opacity-50 min-w-[180px]"
1799
1798
  data-state="closed"
1799
+ id="_r_42_-form-item"
1800
1800
  type="button"
1801
1801
  >
1802
1802
  <span
package/src/utils/sets.ts CHANGED
@@ -27,4 +27,17 @@ export const Sets = {
27
27
  }
28
28
  return true;
29
29
  },
30
+
31
+ /**
32
+ * Return a new set with `item` toggled — removed if present, added if not.
33
+ */
34
+ toggle<T>(set: Set<T>, item: T): Set<T> {
35
+ const result = new Set(set);
36
+ if (result.has(item)) {
37
+ result.delete(item);
38
+ } else {
39
+ result.add(item);
40
+ }
41
+ return result;
42
+ },
30
43
  };