@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.23.4-dev0",
3
+ "version": "0.23.4-dev5",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -209,7 +209,7 @@
209
209
  "@types/react": "^19.2.10",
210
210
  "@types/react-dom": "^19.2.3",
211
211
  "@types/timestring": "^6.0.5",
212
- "@vitejs/plugin-react": "^5.1.4",
212
+ "@vitejs/plugin-react": "^5.2.0",
213
213
  "babel-plugin-react-compiler": "19.1.0-rc.3",
214
214
  "blob-polyfill": "^7.0.20220408",
215
215
  "cross-env": "^7.0.3",
@@ -226,11 +226,11 @@
226
226
  "storybook": "^10.2.12",
227
227
  "stylelint": "^16.26.1",
228
228
  "stylelint-config-standard": "^36.0.1",
229
- "tailwindcss": "^4.2.0",
229
+ "tailwindcss": "^4.2.2",
230
230
  "vega-typings": "^2.1.0",
231
231
  "vite": "npm:rolldown-vite@7.3.1",
232
232
  "vite-plugin-top-level-await": "^1.6.0",
233
- "vite-plugin-wasm": "^3.5.0",
233
+ "vite-plugin-wasm": "^3.6.0",
234
234
  "vitest": "^3.2.4"
235
235
  }
236
236
  }
@@ -22,7 +22,10 @@ import React, { memo } from "react";
22
22
  import { useLocale } from "react-aria";
23
23
 
24
24
  import { Table } from "@/components/ui/table";
25
- import type { GetRowIds } from "@/plugins/impl/DataTablePlugin";
25
+ import type {
26
+ CalculateTopKRows,
27
+ GetRowIds,
28
+ } from "@/plugins/impl/DataTablePlugin";
26
29
  import { cn } from "@/utils/cn";
27
30
  import {
28
31
  PANEL_TYPES,
@@ -89,6 +92,7 @@ interface DataTableProps<TData> extends Partial<ExportActionProps> {
89
92
  showFilters?: boolean;
90
93
  filters?: ColumnFiltersState;
91
94
  onFiltersChange?: OnChangeFn<ColumnFiltersState>;
95
+ calculateTopKRows?: CalculateTopKRows;
92
96
  reloading?: boolean;
93
97
  // Columns
94
98
  freezeColumnsLeft?: string[];
@@ -139,6 +143,7 @@ const DataTableInternal = <TData,>({
139
143
  showFilters = false,
140
144
  filters,
141
145
  onFiltersChange,
146
+ calculateTopKRows,
142
147
  reloading,
143
148
  freezeColumnsLeft,
144
149
  freezeColumnsRight,
@@ -282,7 +287,11 @@ const DataTableInternal = <TData,>({
282
287
 
283
288
  return (
284
289
  <div className={cn(wrapperClassName, "flex flex-col space-y-1")}>
285
- <FilterPills filters={filters} table={table} />
290
+ <FilterPills
291
+ filters={filters}
292
+ table={table}
293
+ calculateTopKRows={calculateTopKRows}
294
+ />
286
295
  <CellSelectionProvider>
287
296
  <div
288
297
  part="table-wrapper"
@@ -0,0 +1,238 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ "use no memo";
3
+
4
+ import type { Column } from "@tanstack/react-table";
5
+ import { ChevronDownIcon } from "lucide-react";
6
+ import { useMemo, useState } from "react";
7
+ import { useAsyncData } from "@/hooks/useAsyncData";
8
+ import { ErrorBanner } from "@/plugins/impl/common/error-banner";
9
+ import type { CalculateTopKRows } from "@/plugins/impl/DataTablePlugin";
10
+ import { cn } from "@/utils/cn";
11
+ import { Logger } from "@/utils/Logger";
12
+ import { Sets } from "@/utils/sets";
13
+ import { smartMatch } from "@/utils/smartMatch";
14
+ import { Spinner } from "../icons/spinner";
15
+ import { Button } from "../ui/button";
16
+ import { Checkbox } from "../ui/checkbox";
17
+ import {
18
+ Command,
19
+ CommandEmpty,
20
+ CommandInput,
21
+ CommandItem,
22
+ CommandList,
23
+ } from "../ui/command";
24
+ import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
25
+ import { SentinelCell } from "./sentinel-cell";
26
+ import { detectSentinel, stringifyUnknownValue } from "./utils";
27
+
28
+ const TOP_K_ROWS = 30;
29
+
30
+ interface Props<TData, TValue> {
31
+ column: Column<TData, TValue>;
32
+ calculateTopKRows?: CalculateTopKRows;
33
+ chosenValues: unknown[];
34
+ onChange: (values: unknown[]) => void;
35
+ }
36
+
37
+ export const FilterByValuesPicker = <TData, TValue>({
38
+ column,
39
+ calculateTopKRows,
40
+ chosenValues,
41
+ onChange,
42
+ }: Props<TData, TValue>) => {
43
+ const [open, setOpen] = useState(false);
44
+
45
+ const chosenValuesSet = useMemo(() => new Set(chosenValues), [chosenValues]);
46
+
47
+ const selectedValuesStr = useMemo(() => {
48
+ if (chosenValuesSet.size === 0) {
49
+ return "Select values…";
50
+ }
51
+ const items = [...chosenValuesSet].map((v) =>
52
+ stringifyUnknownValue({ value: v }),
53
+ );
54
+ return `[${items.join(", ")}]`;
55
+ }, [chosenValuesSet]);
56
+
57
+ return (
58
+ <Popover open={open} onOpenChange={setOpen}>
59
+ <PopoverTrigger asChild={true}>
60
+ <Button
61
+ variant="outline"
62
+ size="xs"
63
+ className="h-6 mb-1 w-full justify-between font-normal"
64
+ >
65
+ <span
66
+ className={cn(
67
+ "truncate",
68
+ chosenValuesSet.size === 0 && "text-muted-foreground",
69
+ )}
70
+ >
71
+ {selectedValuesStr}
72
+ </span>
73
+ <ChevronDownIcon className="h-4 w-4 opacity-50 shrink-0" />
74
+ </Button>
75
+ </PopoverTrigger>
76
+ <PopoverContent className="w-80 p-0">
77
+ <PickerBody
78
+ column={column}
79
+ calculateTopKRows={calculateTopKRows}
80
+ chosenValues={chosenValuesSet}
81
+ onChange={onChange}
82
+ />
83
+ </PopoverContent>
84
+ </Popover>
85
+ );
86
+ };
87
+
88
+ interface PickerBodyProps<TData, TValue> {
89
+ column: Column<TData, TValue>;
90
+ calculateTopKRows?: CalculateTopKRows;
91
+ chosenValues: Set<unknown>;
92
+ onChange: (values: unknown[]) => void;
93
+ }
94
+
95
+ const PickerBody = <TData, TValue>({
96
+ column,
97
+ calculateTopKRows,
98
+ chosenValues,
99
+ onChange,
100
+ }: PickerBodyProps<TData, TValue>) => {
101
+ const [query, setQuery] = useState<string>("");
102
+
103
+ const { data, isPending, error } = useAsyncData(async () => {
104
+ if (!calculateTopKRows) {
105
+ return null;
106
+ }
107
+ const res = await calculateTopKRows({ column: column.id, k: TOP_K_ROWS });
108
+ return res.data;
109
+ }, [calculateTopKRows, column.id]);
110
+
111
+ const filteredData = useMemo(() => {
112
+ if (!data) {
113
+ return [];
114
+ }
115
+ try {
116
+ // try to do includes and also smart match for prefixes
117
+ return data.filter(([value, _count]) => {
118
+ if (value === undefined) {
119
+ return false;
120
+ }
121
+ const str = String(value);
122
+ return (
123
+ smartMatch(query, str) ||
124
+ str.toLowerCase().includes(query.toLowerCase())
125
+ );
126
+ });
127
+ } catch (error_) {
128
+ Logger.error("Error filtering data", error_);
129
+ return [];
130
+ }
131
+ }, [data, query]);
132
+
133
+ const handleToggle = (value: unknown) => {
134
+ onChange([...Sets.toggle(chosenValues, value)]);
135
+ };
136
+
137
+ const allVisibleChecked =
138
+ filteredData.length > 0 &&
139
+ filteredData.every(([value]) => chosenValues.has(value));
140
+
141
+ const selectAllState: boolean | "indeterminate" = allVisibleChecked
142
+ ? true
143
+ : chosenValues.size > 0
144
+ ? "indeterminate"
145
+ : false;
146
+
147
+ const handleToggleAll = () => {
148
+ if (!data) {
149
+ return;
150
+ }
151
+ const next = new Set(chosenValues);
152
+ if (allVisibleChecked) {
153
+ for (const [value] of filteredData) {
154
+ next.delete(value);
155
+ }
156
+ } else {
157
+ for (const [value] of filteredData) {
158
+ next.add(value);
159
+ }
160
+ }
161
+ onChange([...next]);
162
+ };
163
+
164
+ if (isPending) {
165
+ return <Spinner size="medium" className="mx-auto mt-12 mb-10" />;
166
+ }
167
+
168
+ if (error) {
169
+ return <ErrorBanner error={error} className="my-10 mx-4" />;
170
+ }
171
+
172
+ if (!data) {
173
+ return (
174
+ <div className="py-6 px-4 text-sm text-muted-foreground text-center">
175
+ No values available
176
+ </div>
177
+ );
178
+ }
179
+
180
+ return (
181
+ <Command className="text-sm outline-hidden" shouldFilter={false}>
182
+ <CommandInput
183
+ placeholder={`Search among the top ${data.length} values`}
184
+ autoFocus={true}
185
+ onValueChange={(value) => setQuery(value.trim())}
186
+ />
187
+ <CommandEmpty>No results found.</CommandEmpty>
188
+ <CommandList>
189
+ {filteredData.length > 0 && (
190
+ <CommandItem
191
+ value="__select-all__"
192
+ className="border-b rounded-none px-3"
193
+ onSelect={handleToggleAll}
194
+ >
195
+ <Checkbox
196
+ checked={selectAllState}
197
+ aria-label="Select all"
198
+ className="mr-3 h-3.5 w-3.5"
199
+ />
200
+ <span className="font-bold flex-1">{column.id}</span>
201
+ <span className="font-bold">Count</span>
202
+ </CommandItem>
203
+ )}
204
+ {filteredData.map(([value, count]) => {
205
+ const isSelected = chosenValues.has(value);
206
+ const valueString = stringifyUnknownValue({ value });
207
+ const sentinel = detectSentinel(
208
+ value,
209
+ column.columnDef.meta?.dataType,
210
+ );
211
+ return (
212
+ <CommandItem
213
+ key={valueString}
214
+ value={valueString}
215
+ className="not-last:border-b rounded-none px-3"
216
+ onSelect={() => handleToggle(value)}
217
+ >
218
+ <Checkbox
219
+ checked={isSelected}
220
+ aria-label="Select row"
221
+ className="mr-3 h-3.5 w-3.5"
222
+ />
223
+ <span className="flex-1 overflow-hidden max-h-20 line-clamp-3">
224
+ {sentinel ? <SentinelCell sentinel={sentinel} /> : valueString}
225
+ </span>
226
+ <span className="ml-3">{count}</span>
227
+ </CommandItem>
228
+ );
229
+ })}
230
+ </CommandList>
231
+ {data.length === TOP_K_ROWS && (
232
+ <span className="text-xs text-muted-foreground py-1.5 text-center">
233
+ Only showing the top {TOP_K_ROWS} values
234
+ </span>
235
+ )}
236
+ </Command>
237
+ );
238
+ };