@marimo-team/islands 0.23.10-dev8 → 0.23.10

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 (188) hide show
  1. package/dist/{ConnectedDataExplorerComponent-CyV83R2m.js → ConnectedDataExplorerComponent-Z3RP7Vmm.js} +31 -31
  2. package/dist/{ErrorBoundary-rULOrC_p.js → ErrorBoundary-DpbaKVv7.js} +1 -1
  3. package/dist/{any-language-editor-CiES2a2h.js → any-language-editor-CAgFD4Kd.js} +20 -20
  4. package/dist/assets/__vite-browser-external-eshhtsgZ.js +1 -0
  5. package/dist/assets/worker-CC0Oul9k.js +73 -0
  6. package/dist/{chat-ui-K84W5ecY.js → chat-ui-C4-GMW9y.js} +77 -77
  7. package/dist/{check-DTbrK0zt.js → check-BCaJeT-J.js} +1 -1
  8. package/dist/{chunk-5FQGJX7Z-BNjes6Yx.js → chunk-5FQGJX7Z-BbqSm5gU.js} +1227 -1222
  9. package/dist/{code-block-37QAKDTI-m92Yc8pv.js → code-block-37QAKDTI-O6IH6wAi.js} +1 -1
  10. package/dist/{code-visibility-ChnpTxqU.js → code-visibility-DrFozVlm.js} +1943 -1223
  11. package/dist/{copy-5jQ_kGE1.js → copy-UqRYxiOg.js} +1 -1
  12. package/dist/dist-7QfXoMdB.js +5 -0
  13. package/dist/{dist-DgnE8F-r.js → dist-A2846XWO.js} +1 -1
  14. package/dist/dist-BEXXyZig.js +5 -0
  15. package/dist/{dist-B3pZ0Ab6.js → dist-BR_gyG9L.js} +3 -3
  16. package/dist/{dist-CcXxepx6.js → dist-BSAt6RhH.js} +27 -27
  17. package/dist/{dist-Bde4a2kU.js → dist-BY018Paw.js} +8 -8
  18. package/dist/dist-BYj57OV4.js +5 -0
  19. package/dist/{dist-CUCNs1ja.js → dist-BaoDKvdy.js} +2 -2
  20. package/dist/{dist-Cy1WxgBD.js → dist-Bf7SHuNp.js} +5 -5
  21. package/dist/{dist-Bz_sYWbr.js → dist-Bk75fBZA.js} +2 -2
  22. package/dist/dist-BlSvQzNr.js +5 -0
  23. package/dist/{dist-C5VC_yzu.js → dist-BzEzfugY.js} +1 -1
  24. package/dist/dist-CCBlxAgS.js +8 -0
  25. package/dist/dist-CIDTVIUf.js +5 -0
  26. package/dist/{dist-CLUtPrdy.js → dist-CIYBwstr.js} +1 -1
  27. package/dist/{dist-BotSqB48.js → dist-C_Y3oV3C.js} +12 -12
  28. package/dist/{dist-BTfv03uy.js → dist-CcWX6tmx.js} +2 -2
  29. package/dist/{dist-BhM8gdSO.js → dist-CoXAujgg.js} +4 -4
  30. package/dist/{dist-4j4c7bjm.js → dist-CpxNdDkw.js} +3 -3
  31. package/dist/dist-CqQyhAM8.js +8 -0
  32. package/dist/dist-CwRu2Xzh.js +5 -0
  33. package/dist/{dist-BcuoonNH.js → dist-CxJDU6Bh.js} +9 -9
  34. package/dist/{dist-DxvORzUR.js → dist-D-W5ny5a.js} +8 -8
  35. package/dist/dist-D8CDTVgf.js +6 -0
  36. package/dist/dist-D8DNB0nO.js +8 -0
  37. package/dist/dist-DL6N_q-A.js +5 -0
  38. package/dist/{dist-BbbIBDiQ.js → dist-DMjWuVs8.js} +1 -1
  39. package/dist/dist-DOFbNV_b.js +8 -0
  40. package/dist/dist-DPrYzMY0.js +6 -0
  41. package/dist/{dist-h2c8sZvT.js → dist-DZORgqKY.js} +1 -1
  42. package/dist/{dist-B3P2fFpz.js → dist-DZo4nSS0.js} +14 -14
  43. package/dist/{dist-D4CewLk6.js → dist-Dax--nl9.js} +1 -1
  44. package/dist/{dist-DRfcqpxJ.js → dist-DgGbNavJ.js} +2 -2
  45. package/dist/{dist-C1BYNeCR.js → dist-Dk6PV_d3.js} +10 -10
  46. package/dist/{dist-fQ0ViXGs.js → dist-Dv_Y15yk.js} +107 -107
  47. package/dist/{dist-Bfwsv11D.js → dist-DyyjKEYf.js} +2 -2
  48. package/dist/{dist-p2qyWijU.js → dist-GZXUmt0b.js} +2 -2
  49. package/dist/{dist-CLJWPTX2.js → dist-LTU8Hdvn.js} +3 -3
  50. package/dist/{dist-DqAWR3CS.js → dist-M9Vag9Y0.js} +20 -20
  51. package/dist/{dist-DNdhYsgW.js → dist-U4F-tbMs.js} +79 -62
  52. package/dist/{dist-RqXTaiir.js → dist-abid3KgM.js} +11 -11
  53. package/dist/dist-cdmMjgsn.js +5 -0
  54. package/dist/dist-hT4QzYX-.js +1247 -0
  55. package/dist/{dist-luvabDEB.js → dist-t9Kf7xqC.js} +2 -2
  56. package/dist/{error-banner-5bz0L9hS.js → error-banner-CJXYJ6Sb.js} +11 -6
  57. package/dist/esm-BaH2eg5-.js +1171 -0
  58. package/dist/{esm-Duie8iU-.js → esm-ga2Bf3O2.js} +43 -43
  59. package/dist/{extends-CkydH1Q5.js → extends-D_hDsj6R.js} +4 -4
  60. package/dist/files/wasm-intro.py +11 -8
  61. package/dist/{formats-DHxc-FdY.js → formats-C4wO47tk.js} +1 -1
  62. package/dist/{glide-data-editor-CgIxTzAP.js → glide-data-editor-BPkCPs7L.js} +255 -255
  63. package/dist/{html-to-image-B93KtAVY.js → html-to-image-BwfcJJxA.js} +2412 -2282
  64. package/dist/{input-_2sjvfne.js → input-OdWHkobi.js} +263 -245
  65. package/dist/{label-LWtdw5i8.js → label-CC4ytI1X.js} +1 -1
  66. package/dist/main.js +7630 -7558
  67. package/dist/{mermaid-4DMBBIKO-DIdL224_.js → mermaid-4DMBBIKO-Eg3D4FVz.js} +1 -1
  68. package/dist/{mermaid-lXOw5Py9.js → mermaid-BotvIKg9.js} +4 -4
  69. package/dist/{process-output-BOXi9fnS.js → process-output-DsloEDna.js} +3 -3
  70. package/dist/{reveal-component-OrljHylW.js → reveal-component-3YQBHWuD.js} +698 -620
  71. package/dist/{spec-B96zNUEA.js → spec-X7FwLJni.js} +4 -4
  72. package/dist/{strings-Bu3vlb6W.js → strings-J57tzLr3.js} +47 -46
  73. package/dist/style.css +1 -1
  74. package/dist/{toDate-x-WRDCH7.js → toDate-d8RCRrRd.js} +2 -2
  75. package/dist/{tooltip-C5FYOpQc.js → tooltip-DpcyNkQ2.js} +2 -2
  76. package/dist/{types-CVvp1fKr.js → types-ChtMFmZ2.js} +1 -1
  77. package/dist/{useAsyncData-iRgKDT5s.js → useAsyncData-PonK__yh.js} +1 -1
  78. package/dist/{useDateFormatter-BA4FCquG.js → useDateFormatter-QB-3MpYr.js} +2 -2
  79. package/dist/{useDeepCompareMemoize-CkQ57VS2.js → useDeepCompareMemoize-D3NGWke6.js} +1 -1
  80. package/dist/{useLifecycle-BBO9PIph.js → useLifecycle-00mO3OSS.js} +2 -2
  81. package/dist/{useTheme-DHIrRQOe.js → useTheme-DEhDzATN.js} +1 -1
  82. package/dist/{vega-component-Dq-SH463.js → vega-component-DGPUhbDs.js} +8 -8
  83. package/dist/{zod-CoBiJ5v4.js → zod-aLSua2NL.js} +24 -23
  84. package/package.json +4 -3
  85. package/src/__tests__/setup.ts +16 -0
  86. package/src/components/chat/chat-display.tsx +2 -2
  87. package/src/components/data-table/TableBottomBar.tsx +1 -15
  88. package/src/components/data-table/TableTopBar.tsx +8 -13
  89. package/src/components/data-table/__tests__/TableBottomBar.test.tsx +6 -12
  90. package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +227 -0
  91. package/src/components/data-table/column-visibility-dropdown.tsx +204 -0
  92. package/src/components/data-table/data-table.tsx +1 -1
  93. package/src/components/data-table/filter-by-values-picker.tsx +39 -17
  94. package/src/components/data-table/filter-pills.tsx +1 -1
  95. package/src/components/datasources/__tests__/filter-empty.test.ts +183 -0
  96. package/src/components/datasources/datasources.tsx +92 -3
  97. package/src/components/editor/cell/code/cell-editor.tsx +70 -32
  98. package/src/components/editor/cell/code/code-placeholder.css +18 -0
  99. package/src/components/editor/cell/code/code-placeholder.tsx +47 -0
  100. package/src/components/editor/cell/code/language-toggle.tsx +7 -1
  101. package/src/components/editor/chrome/wrapper/app-chrome.tsx +97 -52
  102. package/src/components/editor/chrome/wrapper/lazy-panels.ts +91 -0
  103. package/src/components/editor/chrome/wrapper/sidebar.tsx +2 -0
  104. package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +9 -2
  105. package/src/components/editor/connections/storage/__tests__/as-code.test.ts +62 -27
  106. package/src/components/editor/connections/storage/add-storage-form.tsx +5 -1
  107. package/src/components/editor/connections/storage/as-code.ts +20 -6
  108. package/src/components/editor/documentation.css +35 -0
  109. package/src/components/editor/file-tree/file-explorer.tsx +8 -18
  110. package/src/components/editor/file-tree/tree-actions.tsx +46 -1
  111. package/src/components/editor/notebook-cell.tsx +3 -3
  112. package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +20 -0
  113. package/src/components/editor/renderers/slides-layout/types.ts +1 -0
  114. package/src/components/markdown/__tests__/markdown-renderer.test.tsx +43 -0
  115. package/src/components/markdown/markdown-renderer.tsx +24 -1
  116. package/src/components/slides/__tests__/minimap-actions.test.tsx +166 -0
  117. package/src/components/slides/__tests__/reveal-component.test.ts +425 -0
  118. package/src/components/slides/minimap.tsx +127 -10
  119. package/src/components/slides/reveal-component.tsx +287 -61
  120. package/src/components/slides/slide-cell-view.tsx +26 -2
  121. package/src/components/slides/slide-form.tsx +26 -4
  122. package/src/components/storage/__tests__/storage-inspector.test.ts +180 -0
  123. package/src/components/storage/storage-inspector.tsx +535 -81
  124. package/src/components/ui/combobox.tsx +51 -32
  125. package/src/components/ui/number-field.tsx +5 -1
  126. package/src/components/ui/reorderable-list.tsx +13 -0
  127. package/src/components/ui/select-core/__tests__/use-select-list.test.ts +294 -0
  128. package/src/components/ui/select-core/__tests__/utils.test.ts +222 -0
  129. package/src/components/ui/select-core/index.ts +16 -0
  130. package/src/components/ui/select-core/option-row.tsx +33 -0
  131. package/src/components/ui/select-core/render-slot.ts +20 -0
  132. package/src/components/ui/select-core/select-list.tsx +248 -0
  133. package/src/components/ui/select-core/types.ts +44 -0
  134. package/src/components/ui/select-core/use-select-list.ts +347 -0
  135. package/src/components/ui/select-core/utils.ts +121 -0
  136. package/src/components/ui/toast.tsx +1 -0
  137. package/src/core/cells/__tests__/document-changes.test.ts +2 -2
  138. package/src/core/cells/__tests__/focus.test.ts +45 -2
  139. package/src/core/cells/focus.ts +1 -1
  140. package/src/core/cells/scrollCellIntoView.ts +4 -0
  141. package/src/core/cells/utils.ts +1 -1
  142. package/src/core/codemirror/__tests__/editor-mount-scheduler.test.ts +143 -0
  143. package/src/core/codemirror/copilot/getCodes.ts +3 -1
  144. package/src/core/codemirror/editor-mount-scheduler.ts +101 -0
  145. package/src/core/codemirror/language/languages/python.ts +2 -0
  146. package/src/core/codemirror/language/languages/sql/utils.ts +3 -1
  147. package/src/core/codemirror/lsp/__tests__/markdown-renderer.test.ts +41 -0
  148. package/src/core/codemirror/lsp/markdown-renderer.ts +59 -0
  149. package/src/core/datasets/data-source-connections.ts +2 -0
  150. package/src/core/errors/errors.ts +1 -1
  151. package/src/core/storage/__tests__/state.test.ts +49 -0
  152. package/src/core/storage/__tests__/useStorageEntries.test.tsx +199 -3
  153. package/src/core/storage/state.ts +124 -13
  154. package/src/core/storage/types.ts +6 -0
  155. package/src/core/wasm/worker/bootstrap.ts +12 -4
  156. package/src/plugins/core/__test__/registerReactComponent.test.ts +96 -0
  157. package/src/plugins/core/registerReactComponent.tsx +42 -10
  158. package/src/plugins/impl/MultiselectPlugin.tsx +19 -142
  159. package/src/plugins/impl/SearchableSelect.tsx +16 -97
  160. package/src/plugins/impl/SliderPlugin.tsx +132 -18
  161. package/src/plugins/impl/__tests__/DropdownPlugin.test.tsx +5 -2
  162. package/src/plugins/impl/__tests__/MultiSelectPlugin.test.ts +1 -1
  163. package/src/plugins/impl/__tests__/SliderPlugin.test.tsx +278 -8
  164. package/src/utils/__tests__/numbers.test.ts +20 -0
  165. package/src/utils/errors.ts +9 -0
  166. package/src/utils/lazy.ts +6 -1
  167. package/src/utils/numbers.ts +27 -0
  168. package/src/utils/schedule-task.ts +71 -0
  169. package/dist/assets/__vite-browser-external-Ci2ZQfXU.js +0 -1
  170. package/dist/assets/worker-ip3AI_sN.js +0 -73
  171. package/dist/dist-0Fif7jnk.js +0 -5
  172. package/dist/dist-B5h_9sHB.js +0 -6
  173. package/dist/dist-B9M6R5ye.js +0 -5
  174. package/dist/dist-BCt3tnck.js +0 -8
  175. package/dist/dist-BUIJwMwn.js +0 -8
  176. package/dist/dist-BpquMd3k.js +0 -5
  177. package/dist/dist-BzJsqYfz.js +0 -5
  178. package/dist/dist-CA5ELXAf.js +0 -6
  179. package/dist/dist-CLBRs6Uv.js +0 -5
  180. package/dist/dist-CStVCMbq.js +0 -5
  181. package/dist/dist-CZRIEY3Y.js +0 -8
  182. package/dist/dist-CuUHbFD0.js +0 -5
  183. package/dist/dist-DV7Iabxb.js +0 -8
  184. package/dist/dist-DhHh0jLg.js +0 -1247
  185. package/dist/dist-DuEeHMvL.js +0 -5
  186. package/dist/esm-CCuYCd3R.js +0 -1171
  187. package/src/plugins/impl/multiselectFilterFn.tsx +0 -22
  188. /package/src/components/{data-table → ui}/value-chips.tsx +0 -0
@@ -0,0 +1,227 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import {
4
+ type ColumnDef,
5
+ getCoreRowModel,
6
+ useReactTable,
7
+ } from "@tanstack/react-table";
8
+ import { fireEvent, render, screen } from "@testing-library/react";
9
+ import { beforeAll, describe, expect, it } from "vitest";
10
+ import { ColumnVisibilityDropdown } from "../column-visibility-dropdown";
11
+ import { INDEX_COLUMN_NAME, SELECT_COLUMN_ID } from "../types";
12
+
13
+ beforeAll(() => {
14
+ global.HTMLElement.prototype.scrollIntoView = () => {
15
+ // jsdom does not implement scrollIntoView; cmdk calls it on selection.
16
+ };
17
+ if (!global.HTMLElement.prototype.hasPointerCapture) {
18
+ global.HTMLElement.prototype.hasPointerCapture = () => false;
19
+ }
20
+ });
21
+
22
+ type Row = Record<string, unknown>;
23
+
24
+ const TEST_COLUMNS: ColumnDef<Row>[] = [
25
+ { id: SELECT_COLUMN_ID, accessorKey: SELECT_COLUMN_ID },
26
+ { id: INDEX_COLUMN_NAME, accessorKey: INDEX_COLUMN_NAME },
27
+ { id: "__m_column__0", accessorKey: "__m_column__0" },
28
+ {
29
+ id: "customer_name",
30
+ accessorKey: "customer_name",
31
+ meta: { dataType: "string" },
32
+ },
33
+ { id: "cust_age", accessorKey: "cust_age", meta: { dataType: "integer" } },
34
+ {
35
+ id: "order_total",
36
+ accessorKey: "order_total",
37
+ meta: { dataType: "number" },
38
+ },
39
+ ];
40
+
41
+ interface HarnessProps {
42
+ initiallyHidden?: string[];
43
+ nonHideable?: string[];
44
+ }
45
+
46
+ function Harness({ initiallyHidden = [], nonHideable = [] }: HarnessProps) {
47
+ const table = useReactTable<Row>({
48
+ data: [],
49
+ columns: TEST_COLUMNS.map((column) =>
50
+ nonHideable.includes(column.id as string)
51
+ ? { ...column, enableHiding: false }
52
+ : column,
53
+ ),
54
+ getCoreRowModel: getCoreRowModel(),
55
+ locale: "en-US",
56
+ initialState: {
57
+ columnVisibility: Object.fromEntries(
58
+ initiallyHidden.map((id) => [id, false]),
59
+ ),
60
+ },
61
+ });
62
+ return <ColumnVisibilityDropdown table={table} />;
63
+ }
64
+
65
+ function renderAndOpen(props?: HarnessProps) {
66
+ const result = render(<Harness {...(props ?? {})} />);
67
+ fireEvent.click(screen.getByTestId("column-visibility-trigger"));
68
+ return result;
69
+ }
70
+
71
+ function getOptionTexts(): string[] {
72
+ return screen.getAllByRole("option").map((el) => el.textContent ?? "");
73
+ }
74
+
75
+ function getColumnOption(name: string): HTMLElement {
76
+ const option = screen
77
+ .getAllByRole("option")
78
+ .find((el) => el.textContent === name);
79
+ if (!option) {
80
+ throw new Error(`No option for column ${name}`);
81
+ }
82
+ return option;
83
+ }
84
+
85
+ function getSearchInput() {
86
+ return screen.getByPlaceholderText("Search columns...");
87
+ }
88
+
89
+ describe("ColumnVisibilityDropdown", () => {
90
+ it("renders user columns and excludes select/index/nameless columns", () => {
91
+ renderAndOpen();
92
+ expect(screen.getByText("customer_name")).toBeInTheDocument();
93
+ expect(screen.getByText("cust_age")).toBeInTheDocument();
94
+ expect(screen.getByText("order_total")).toBeInTheDocument();
95
+ expect(screen.queryByText(SELECT_COLUMN_ID)).not.toBeInTheDocument();
96
+ expect(screen.queryByText(INDEX_COLUMN_NAME)).not.toBeInTheDocument();
97
+ expect(screen.queryByText("__m_column__0")).not.toBeInTheDocument();
98
+ });
99
+
100
+ it("lists hidden columns before shown columns", () => {
101
+ renderAndOpen({ initiallyHidden: ["cust_age"] });
102
+ expect(getOptionTexts()).toEqual([
103
+ "Show all",
104
+ "cust_age",
105
+ "customer_name",
106
+ "order_total",
107
+ ]);
108
+ });
109
+
110
+ it("toggling a hidden column flips the icon without moving the row", () => {
111
+ renderAndOpen({ initiallyHidden: ["cust_age"] });
112
+ expect(
113
+ getColumnOption("cust_age").querySelector(".lucide-eye-off"),
114
+ ).not.toBeNull();
115
+
116
+ fireEvent.click(getColumnOption("cust_age"));
117
+
118
+ expect(getOptionTexts()).toEqual([
119
+ "Show all",
120
+ "cust_age",
121
+ "customer_name",
122
+ "order_total",
123
+ ]);
124
+ expect(
125
+ getColumnOption("cust_age").querySelector(".lucide-eye-off"),
126
+ ).toBeNull();
127
+ });
128
+
129
+ it("re-sorts on reopen after a toggle", () => {
130
+ renderAndOpen({ initiallyHidden: ["cust_age"] });
131
+ // Hide customer_name: it stays in the shown section until reopen.
132
+ fireEvent.click(getColumnOption("customer_name"));
133
+ expect(getOptionTexts()).toEqual([
134
+ "Show all",
135
+ "cust_age",
136
+ "customer_name",
137
+ "order_total",
138
+ ]);
139
+
140
+ const trigger = screen.getByTestId("column-visibility-trigger");
141
+ fireEvent.click(trigger);
142
+ fireEvent.click(trigger);
143
+
144
+ // Reopen sorts both hidden columns first, preserving table order.
145
+ expect(getOptionTexts()).toEqual([
146
+ "Show all",
147
+ "customer_name",
148
+ "cust_age",
149
+ "order_total",
150
+ ]);
151
+ });
152
+
153
+ it("filters columns with smartMatch", () => {
154
+ renderAndOpen();
155
+ fireEvent.change(getSearchInput(), { target: { value: "cust" } });
156
+ expect(screen.getByText("customer_name")).toBeInTheDocument();
157
+ expect(screen.getByText("cust_age")).toBeInTheDocument();
158
+ expect(screen.queryByText("order_total")).not.toBeInTheDocument();
159
+ });
160
+
161
+ it("shows 'No results.' when nothing matches", () => {
162
+ renderAndOpen();
163
+ fireEvent.change(getSearchInput(), { target: { value: "xyz" } });
164
+ expect(screen.getByText("No results.")).toBeInTheDocument();
165
+ expect(screen.queryByText("customer_name")).not.toBeInTheDocument();
166
+ });
167
+
168
+ it("disables 'Show all' when no columns are hidden", () => {
169
+ renderAndOpen();
170
+ expect(getColumnOption("Show all")).toHaveAttribute(
171
+ "aria-disabled",
172
+ "true",
173
+ );
174
+ });
175
+
176
+ it("'Show all' restores hidden columns", () => {
177
+ renderAndOpen({ initiallyHidden: ["cust_age", "order_total"] });
178
+ fireEvent.click(getColumnOption("Show all"));
179
+
180
+ expect(
181
+ getColumnOption("cust_age").querySelector(".lucide-eye-off"),
182
+ ).toBeNull();
183
+ expect(
184
+ getColumnOption("order_total").querySelector(".lucide-eye-off"),
185
+ ).toBeNull();
186
+ expect(getColumnOption("Show all")).toHaveAttribute(
187
+ "aria-disabled",
188
+ "true",
189
+ );
190
+ });
191
+
192
+ it("hides and shows matching columns via bulk actions while searching", () => {
193
+ renderAndOpen();
194
+ fireEvent.change(getSearchInput(), { target: { value: "cust" } });
195
+ expect(screen.queryByText(/Show all/)).not.toBeInTheDocument();
196
+
197
+ fireEvent.click(getColumnOption("Hide 2 matching"));
198
+ expect(
199
+ getColumnOption("customer_name").querySelector(".lucide-eye-off"),
200
+ ).not.toBeNull();
201
+ expect(
202
+ getColumnOption("cust_age").querySelector(".lucide-eye-off"),
203
+ ).not.toBeNull();
204
+ expect(screen.queryByText(/Hide \d+ matching/)).not.toBeInTheDocument();
205
+
206
+ fireEvent.click(getColumnOption("Show 2 matching"));
207
+ expect(
208
+ getColumnOption("customer_name").querySelector(".lucide-eye-off"),
209
+ ).toBeNull();
210
+ expect(screen.queryByText(/Show \d+ matching/)).not.toBeInTheDocument();
211
+ });
212
+
213
+ it("offers both bulk actions when matches are mixed", () => {
214
+ renderAndOpen({ initiallyHidden: ["cust_age"] });
215
+ fireEvent.change(getSearchInput(), { target: { value: "cust" } });
216
+ expect(screen.getByText(/Hide 1 matching/)).toBeInTheDocument();
217
+ expect(screen.getByText(/Show 1 matching/)).toBeInTheDocument();
218
+ });
219
+
220
+ it("renders non-hideable columns disabled and without an eye toggle", () => {
221
+ renderAndOpen({ nonHideable: ["customer_name"] });
222
+ const option = getColumnOption("customer_name");
223
+ expect(option).toHaveAttribute("aria-disabled", "true");
224
+ expect(option.querySelector(".lucide-eye")).toBeNull();
225
+ expect(option.querySelector(".lucide-eye-off")).toBeNull();
226
+ });
227
+ });
@@ -0,0 +1,204 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ "use no memo";
3
+
4
+ // tanstack/table is not compatible with React compiler
5
+ // https://github.com/TanStack/table/issues/5567
6
+
7
+ import type { Table } from "@tanstack/react-table";
8
+ import { Columns3Icon, EyeIcon, EyeOffIcon } from "lucide-react";
9
+ import React from "react";
10
+ import { ColumnName } from "@/components/datasources/components";
11
+ import { Button } from "@/components/ui/button";
12
+ import {
13
+ Command,
14
+ CommandEmpty,
15
+ CommandInput,
16
+ CommandItem,
17
+ CommandList,
18
+ CommandSeparator,
19
+ } from "@/components/ui/command";
20
+ import {
21
+ Popover,
22
+ PopoverContent,
23
+ PopoverTrigger,
24
+ } from "@/components/ui/popover";
25
+ import { type BulkAction, useSelectList } from "@/components/ui/select-core";
26
+ import type { DataType } from "@/core/kernel/messages";
27
+ import { cn } from "@/utils/cn";
28
+ import { Events } from "@/utils/events";
29
+ import { smartMatchFilter } from "@/utils/smartMatch";
30
+ import { NAMELESS_COLUMN_PREFIX } from "./columns";
31
+ import { INDEX_COLUMN_NAME, SELECT_COLUMN_ID } from "./types";
32
+
33
+ function getUserColumns<TData>(table: Table<TData>) {
34
+ return table
35
+ .getAllLeafColumns()
36
+ .filter(
37
+ (column) =>
38
+ column.id !== SELECT_COLUMN_ID &&
39
+ column.id !== INDEX_COLUMN_NAME &&
40
+ !column.id.startsWith(NAMELESS_COLUMN_PREFIX),
41
+ );
42
+ }
43
+
44
+ export const ColumnVisibilityDropdown = <TData,>({
45
+ table,
46
+ }: {
47
+ table: Table<TData>;
48
+ }) => {
49
+ const userColumns = getUserColumns(table);
50
+ const options = userColumns.map((column) => ({
51
+ value: column.id,
52
+ label: column.id,
53
+ disabled: !column.getCanHide(),
54
+ data: { dataType: column.columnDef.meta?.dataType },
55
+ }));
56
+ // Modeled as a select list over hidden columns: "selected" means hidden, so
57
+ // the hook's pinning floats hidden columns to the top and freezes that order
58
+ // while the menu is open.
59
+ const hiddenIds = userColumns
60
+ .filter((column) => !column.getIsVisible())
61
+ .map((column) => column.id);
62
+
63
+ const applyHidden = (next: string[] | string | null) => {
64
+ const hidden = new Set(Array.isArray(next) ? next : []);
65
+ table.setColumnVisibility((previous) => ({
66
+ ...previous,
67
+ ...Object.fromEntries(
68
+ userColumns.map((column) => [column.id, !hidden.has(column.id)]),
69
+ ),
70
+ }));
71
+ };
72
+
73
+ const list = useSelectList<string>({
74
+ options,
75
+ value: hiddenIds,
76
+ onChange: applyHidden,
77
+ multiple: true,
78
+ filterFn: smartMatchFilter,
79
+ pinSelected: true,
80
+ });
81
+ // With selection modeling hidden columns, select-matching hides the visible
82
+ // matches and deselect-matching shows the hidden ones.
83
+ const matchingActions = list.bulkActions.filter(
84
+ (
85
+ action,
86
+ ): action is Extract<
87
+ BulkAction<string>,
88
+ { kind: "select-matching" | "deselect-matching" }
89
+ > =>
90
+ action.kind === "select-matching" || action.kind === "deselect-matching",
91
+ );
92
+
93
+ return (
94
+ <Popover open={list.open} onOpenChange={list.setOpen}>
95
+ <PopoverTrigger asChild={true}>
96
+ <Button
97
+ variant="text"
98
+ size="xs"
99
+ data-testid="column-visibility-trigger"
100
+ onMouseDown={Events.preventFocus}
101
+ className={cn(
102
+ "print:hidden text-xs gap-1",
103
+ list.open ? "text-primary" : "text-muted-foreground",
104
+ )}
105
+ >
106
+ <Columns3Icon className="w-3.5 h-3.5" />
107
+ Columns
108
+ </Button>
109
+ </PopoverTrigger>
110
+ <PopoverContent className="w-64 p-0" align="end">
111
+ <Command shouldFilter={false}>
112
+ <CommandInput
113
+ placeholder="Search columns..."
114
+ value={list.searchQuery}
115
+ onValueChange={list.setSearchQuery}
116
+ />
117
+ <CommandList>
118
+ <CommandEmpty>No results.</CommandEmpty>
119
+ {list.searchQuery === "" ? (
120
+ <>
121
+ <CommandItem
122
+ value="__show_all__"
123
+ disabled={hiddenIds.length === 0}
124
+ onSelect={() => applyHidden([])}
125
+ className="cursor-pointer"
126
+ >
127
+ <EyeIcon className="w-3 h-3 mr-1.5" />
128
+ Show all
129
+ </CommandItem>
130
+ <CommandSeparator />
131
+ </>
132
+ ) : (
133
+ matchingActions.length > 0 && (
134
+ <>
135
+ {matchingActions.map((action) => (
136
+ <CommandItem
137
+ key={action.kind}
138
+ value={`__bulk_${action.kind}`}
139
+ onSelect={action.run}
140
+ className="cursor-pointer"
141
+ >
142
+ {action.kind === "select-matching" ? (
143
+ <EyeOffIcon className="w-3 h-3 mr-1.5" />
144
+ ) : (
145
+ <EyeIcon className="w-3 h-3 mr-1.5" />
146
+ )}
147
+ {action.kind === "select-matching" ? "Hide" : "Show"}{" "}
148
+ {action.items.length} matching
149
+ </CommandItem>
150
+ ))}
151
+ <CommandSeparator />
152
+ </>
153
+ )
154
+ )}
155
+ {list.visibleOptions.map((option, index) => {
156
+ const hidden = list.isChecked(option.value);
157
+ const { dataType } = option.data as {
158
+ dataType: DataType | undefined;
159
+ };
160
+ const isSectionBoundary =
161
+ index === list.pinnedCount &&
162
+ list.pinnedCount > 0 &&
163
+ list.pinnedCount < list.visibleOptions.length;
164
+ return (
165
+ <React.Fragment key={option.value}>
166
+ {isSectionBoundary && <CommandSeparator />}
167
+ <CommandItem
168
+ value={option.value}
169
+ disabled={option.disabled}
170
+ onSelect={() => list.toggle(option.value)}
171
+ className="flex items-center gap-1.5 cursor-pointer"
172
+ >
173
+ {dataType === undefined ? (
174
+ <span>{option.label}</span>
175
+ ) : (
176
+ <ColumnName
177
+ columnName={option.label}
178
+ dataType={dataType}
179
+ />
180
+ )}
181
+ {!option.disabled && (
182
+ <span
183
+ className={cn(
184
+ "ml-auto",
185
+ hidden ? "text-primary" : "text-muted-foreground",
186
+ )}
187
+ >
188
+ {hidden ? (
189
+ <EyeOffIcon className="w-3 h-3" />
190
+ ) : (
191
+ <EyeIcon className="w-3 h-3" />
192
+ )}
193
+ </span>
194
+ )}
195
+ </CommandItem>
196
+ </React.Fragment>
197
+ );
198
+ })}
199
+ </CommandList>
200
+ </Command>
201
+ </PopoverContent>
202
+ </Popover>
203
+ );
204
+ };
@@ -356,6 +356,7 @@ const DataTableInternal = <TData,>({
356
356
  className={cn(className || "rounded-md border overflow-hidden")}
357
357
  >
358
358
  <TableTopBar
359
+ table={table}
359
360
  enableSearch={enableSearch}
360
361
  searchQuery={searchQuery}
361
362
  onSearchQueryChange={onSearchQueryChange}
@@ -413,7 +414,6 @@ const DataTableInternal = <TData,>({
413
414
  getRowIds={getRowIds}
414
415
  showPageSizeSelector={showPageSizeSelector}
415
416
  tableLoading={reloading}
416
- togglePanel={togglePanel}
417
417
  />
418
418
  </div>
419
419
  </CellSelectionProvider>
@@ -7,6 +7,8 @@ import { useMemo, useState } from "react";
7
7
  import { useAsyncData } from "@/hooks/useAsyncData";
8
8
  import { ErrorBanner } from "@/plugins/impl/common/error-banner";
9
9
  import type { CalculateTopKRows } from "@/plugins/impl/DataTablePlugin";
10
+ import type { Option } from "@/components/ui/select-core";
11
+ import { useSelectList } from "@/components/ui/select-core";
10
12
  import { Logger } from "@/utils/Logger";
11
13
  import { Sets } from "@/utils/sets";
12
14
  import { smartMatch } from "@/utils/smartMatch";
@@ -23,7 +25,7 @@ import {
23
25
  import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
24
26
  import { SentinelCell } from "./sentinel-cell";
25
27
  import { detectSentinel, stringifyUnknownValue } from "./utils";
26
- import { CompactChipRow } from "./value-chips";
28
+ import { CompactChipRow } from "@/components/ui/value-chips";
27
29
 
28
30
  const TOP_K_ROWS = 30;
29
31
 
@@ -101,8 +103,6 @@ export const FilterByValuesList = <TData, TValue>({
101
103
  onChange,
102
104
  creatable = false,
103
105
  }: FilterByValuesListProps<TData, TValue>) => {
104
- const [query, setQuery] = useState<string>("");
105
-
106
106
  const { data, isPending, error } = useAsyncData(async () => {
107
107
  if (!calculateTopKRows) {
108
108
  return null;
@@ -111,27 +111,49 @@ export const FilterByValuesList = <TData, TValue>({
111
111
  return res.data;
112
112
  }, [calculateTopKRows, column.id]);
113
113
 
114
- const filteredData = useMemo(() => {
114
+ const options = useMemo<Array<Option<unknown>>>(() => {
115
115
  if (!data) {
116
116
  return [];
117
117
  }
118
118
  try {
119
- // try to do includes and also smart match for prefixes
120
- return data.filter(([value, _count]) => {
121
- if (value === undefined) {
122
- return false;
123
- }
124
- const str = String(value);
125
- return (
126
- smartMatch(query, str) ||
127
- str.toLowerCase().includes(query.toLowerCase())
128
- );
129
- });
119
+ return data
120
+ .filter(([value]) => value !== undefined)
121
+ .map(([value, count]) => ({
122
+ value,
123
+ label: String(value),
124
+ data: { count },
125
+ }));
130
126
  } catch (error_) {
131
- Logger.error("Error filtering data", error_);
127
+ Logger.error("Error building filter options", error_);
132
128
  return [];
133
129
  }
134
- }, [data, query]);
130
+ }, [data]);
131
+
132
+ const list = useSelectList<unknown>({
133
+ options,
134
+ value: [...chosenValues],
135
+ onChange: (next) => onChange(next as unknown[]),
136
+ multiple: true,
137
+ filterFn: (label, q) =>
138
+ smartMatch(q, label) || label.toLowerCase().includes(q.toLowerCase())
139
+ ? 1
140
+ : 0,
141
+ });
142
+
143
+ const query = list.searchQuery;
144
+ const setQuery = list.setSearchQuery;
145
+
146
+ const filteredData = useMemo<Array<[unknown, number | undefined]>>(
147
+ () =>
148
+ list.visibleOptions.map(
149
+ (o) =>
150
+ [o.value, (o.data as { count: number }).count] as [
151
+ unknown,
152
+ number | undefined,
153
+ ],
154
+ ),
155
+ [list.visibleOptions],
156
+ );
135
157
 
136
158
  // Surface chosen values that aren't in the top-K so they stay visible/uncheckable.
137
159
  // Count is undefined for these rows; the cell renders an em-dash.
@@ -21,7 +21,7 @@ import { AddFilterButton } from "./add-filter-button";
21
21
  import { FilterPillEditor } from "./filter-pill-editor";
22
22
  import { type ColumnFilterValue, formatValue, type Snapshot } from "./filters";
23
23
  import { extractTimezone } from "./types";
24
- import { ChipWithComma, CompactChipRow } from "./value-chips";
24
+ import { ChipWithComma, CompactChipRow } from "@/components/ui/value-chips";
25
25
 
26
26
  interface Props<TData> {
27
27
  filters: ColumnFiltersState | undefined;