@marimo-team/islands 0.23.9-dev9 → 0.23.9

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 (101) hide show
  1. package/dist/{ConnectedDataExplorerComponent-OzrfMM5L.js → ConnectedDataExplorerComponent-CyV83R2m.js} +4 -4
  2. package/dist/assets/__vite-browser-external-Ci2ZQfXU.js +1 -0
  3. package/dist/assets/{worker-CpBbwbQo.js → worker-ip3AI_sN.js} +2 -2
  4. package/dist/{chat-ui-BDI3FMI8.js → chat-ui-ChD4VvCo.js} +3060 -3033
  5. package/dist/{code-visibility-DgHF4q8X.js → code-visibility-BkuwTYAm.js} +1368 -1204
  6. package/dist/{formats-DQ5qjo_Q.js → formats-DHxc-FdY.js} +1 -1
  7. package/dist/{glide-data-editor-DqRY9naW.js → glide-data-editor-BOmK9ETQ.js} +2 -2
  8. package/dist/{html-to-image-CiSinpSR.js → html-to-image-BHv7CEU_.js} +2145 -2153
  9. package/dist/{input-CZD2z6X2.js → input-_2sjvfne.js} +1 -1
  10. package/dist/main.js +680 -705
  11. package/dist/{mermaid-IU93XzmY.js → mermaid-lXOw5Py9.js} +2 -2
  12. package/dist/{process-output-5qJjMRKh.js → process-output-BvySRgli.js} +33 -25
  13. package/dist/{reveal-component-qpHJES_u.js → reveal-component-DeBkkDcg.js} +312 -291
  14. package/dist/{spec-a6DaqW__.js → spec-B96zNUEA.js} +1 -1
  15. package/dist/style.css +1 -1
  16. package/dist/{toDate-ZVVIBmdk.js → toDate-x-WRDCH7.js} +1 -1
  17. package/dist/{useAsyncData-C008zUPi.js → useAsyncData-iRgKDT5s.js} +1 -1
  18. package/dist/{useDeepCompareMemoize-BrA3_n61.js → useDeepCompareMemoize-CkQ57VS2.js} +1 -1
  19. package/dist/{useLifecycle-BNaoJ5a4.js → useLifecycle-BBO9PIph.js} +1 -1
  20. package/dist/{useTheme-7O0YWlE5.js → useTheme-DHIrRQOe.js} +34 -21
  21. package/dist/{vega-component-DJNmOdUj.js → vega-component-Dq-SH463.js} +5 -5
  22. package/package.json +1 -1
  23. package/src/components/ai/__tests__/ai-utils.test.ts +43 -38
  24. package/src/components/ai/ai-model-dropdown.tsx +2 -2
  25. package/src/components/app-config/ai-config.tsx +147 -16
  26. package/src/components/app-config/user-config-form.tsx +37 -1
  27. package/src/components/chat/__tests__/chat-utils.test.ts +269 -0
  28. package/src/components/chat/chat-panel.tsx +38 -5
  29. package/src/components/chat/chat-utils.ts +14 -58
  30. package/src/components/data-table/TableBottomBar.tsx +5 -8
  31. package/src/components/data-table/__tests__/column-explorer.test.tsx +128 -0
  32. package/src/components/data-table/__tests__/header-items.test.tsx +220 -10
  33. package/src/components/data-table/column-explorer-panel/column-explorer.tsx +95 -29
  34. package/src/components/data-table/column-header.tsx +17 -12
  35. package/src/components/data-table/data-table.tsx +4 -0
  36. package/src/components/data-table/export-actions.tsx +19 -12
  37. package/src/components/data-table/header-items.tsx +40 -16
  38. package/src/components/data-table/hooks/use-column-visibility.ts +14 -0
  39. package/src/components/data-table/schemas.ts +2 -2
  40. package/src/components/data-table/table-explorer-panel/table-explorer-panel.tsx +16 -6
  41. package/src/components/databases/display.tsx +2 -0
  42. package/src/components/datasources/__tests__/utils.test.ts +82 -0
  43. package/src/components/datasources/utils.ts +16 -15
  44. package/src/components/editor/Disconnected.tsx +1 -60
  45. package/src/components/editor/__tests__/viewer-banner.test.tsx +89 -0
  46. package/src/components/editor/actions/pair-with-agent-modal.tsx +1 -0
  47. package/src/components/editor/actions/useCellActionButton.tsx +3 -3
  48. package/src/components/editor/actions/useNotebookActions.tsx +5 -2
  49. package/src/components/editor/cell/code/cell-editor.tsx +25 -5
  50. package/src/components/editor/chrome/types.ts +13 -6
  51. package/src/components/editor/chrome/wrapper/app-chrome.tsx +6 -4
  52. package/src/components/editor/chrome/wrapper/footer-items/ai-status.tsx +10 -1
  53. package/src/components/editor/chrome/wrapper/sidebar.tsx +7 -5
  54. package/src/components/editor/errors/auto-fix.tsx +3 -3
  55. package/src/components/editor/header/__tests__/status.test.tsx +0 -15
  56. package/src/components/editor/header/app-header.tsx +1 -4
  57. package/src/components/editor/header/status.tsx +4 -13
  58. package/src/components/editor/navigation/__tests__/navigation.test.ts +15 -0
  59. package/src/components/editor/navigation/navigation.ts +5 -0
  60. package/src/components/editor/output/MarimoErrorOutput.tsx +103 -25
  61. package/src/components/editor/output/MarimoTracebackOutput.tsx +28 -39
  62. package/src/components/editor/renderers/cell-array.tsx +27 -24
  63. package/src/components/editor/renderers/slides-layout/__tests__/compute-slide-cells.test.ts +30 -17
  64. package/src/components/editor/renderers/slides-layout/compute-slide-cells.ts +17 -8
  65. package/src/components/editor/renderers/slides-layout/slides-layout.tsx +10 -12
  66. package/src/components/editor/viewer-banner.tsx +82 -0
  67. package/src/components/slides/minimap.tsx +45 -9
  68. package/src/components/slides/reveal-component.tsx +82 -37
  69. package/src/components/slides/slide-cell-view.tsx +12 -1
  70. package/src/components/slides/slide-form.tsx +11 -3
  71. package/src/components/static-html/static-banner.tsx +28 -22
  72. package/src/core/ai/__tests__/model-registry.test.ts +72 -60
  73. package/src/core/ai/model-registry.ts +33 -28
  74. package/src/core/cells/__tests__/actions.test.ts +48 -0
  75. package/src/core/cells/actions.ts +5 -6
  76. package/src/core/codemirror/__tests__/setup.test.ts +29 -0
  77. package/src/core/codemirror/cells/traceback-decorations.ts +1 -1
  78. package/src/core/codemirror/cm.ts +50 -3
  79. package/src/core/codemirror/completion/hints.ts +4 -1
  80. package/src/core/codemirror/format.ts +1 -0
  81. package/src/core/codemirror/keymaps/vim.ts +63 -0
  82. package/src/core/codemirror/language/languages/sql/sql.ts +1 -0
  83. package/src/core/codemirror/language/languages/sql/utils.ts +2 -0
  84. package/src/core/config/__tests__/config-schema.test.ts +4 -0
  85. package/src/core/config/config-schema.ts +4 -0
  86. package/src/core/config/config.ts +16 -0
  87. package/src/core/edit-app.tsx +3 -0
  88. package/src/core/islands/bootstrap.ts +2 -0
  89. package/src/core/kernel/__tests__/handlers.test.ts +5 -0
  90. package/src/core/websocket/__tests__/useMarimoKernelConnection.test.ts +0 -13
  91. package/src/core/websocket/types.ts +0 -6
  92. package/src/core/websocket/useMarimoKernelConnection.tsx +3 -12
  93. package/src/css/app/Cell.css +0 -1
  94. package/src/plugins/impl/DataTablePlugin.tsx +48 -22
  95. package/src/plugins/impl/chat/ChatPlugin.tsx +7 -1
  96. package/src/plugins/impl/chat/__tests__/chat-ui.test.ts +278 -0
  97. package/src/plugins/impl/chat/chat-ui.tsx +106 -59
  98. package/src/plugins/impl/chat/types.ts +5 -0
  99. package/src/utils/__tests__/json-parser.test.ts +1 -69
  100. package/src/utils/json/json-parser.ts +0 -30
  101. package/dist/assets/__vite-browser-external-CAdMKBac.js +0 -1
@@ -1,6 +1,11 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import type { Column, SortingState } from "@tanstack/react-table";
3
+ import type {
4
+ Column,
5
+ SortDirection,
6
+ SortingState,
7
+ Table,
8
+ } from "@tanstack/react-table";
4
9
  import { fireEvent, render, screen } from "@testing-library/react";
5
10
  import { describe, expect, it, vi } from "vitest";
6
11
  import {
@@ -8,7 +13,23 @@ import {
8
13
  DropdownMenuContent,
9
14
  DropdownMenuTrigger,
10
15
  } from "@/components/ui/dropdown-menu";
11
- import { HideColumn } from "../header-items";
16
+ import {
17
+ ColumnPinning,
18
+ ColumnWrapping,
19
+ CopyColumn,
20
+ DataType,
21
+ FormatOptions,
22
+ HideColumn,
23
+ Sorts,
24
+ } from "../header-items";
25
+
26
+ const renderInMenu = (node: React.ReactNode) =>
27
+ render(
28
+ <DropdownMenu open={true}>
29
+ <DropdownMenuTrigger />
30
+ <DropdownMenuContent>{node}</DropdownMenuContent>
31
+ </DropdownMenu>,
32
+ );
12
33
 
13
34
  describe("multi-column sorting logic", () => {
14
35
  // Extract the core sorting logic to test in isolation
@@ -167,14 +188,6 @@ describe("HideColumn", () => {
167
188
  toggleVisibility,
168
189
  }) as unknown as Column<unknown, unknown>;
169
190
 
170
- const renderInMenu = (node: React.ReactNode) =>
171
- render(
172
- <DropdownMenu open={true}>
173
- <DropdownMenuTrigger />
174
- <DropdownMenuContent>{node}</DropdownMenuContent>
175
- </DropdownMenu>,
176
- );
177
-
178
191
  it("renders 'Hide column' when canHide is true", () => {
179
192
  renderInMenu(<HideColumn column={makeColumn()} />);
180
193
  expect(screen.getByText("Hide column")).toBeInTheDocument();
@@ -192,3 +205,200 @@ describe("HideColumn", () => {
192
205
  expect(toggleVisibility).toHaveBeenCalledWith(false);
193
206
  });
194
207
  });
208
+
209
+ describe("DataType", () => {
210
+ const makeColumn = (dtype?: string) =>
211
+ ({
212
+ columnDef: { meta: dtype === undefined ? {} : { dtype } },
213
+ }) as unknown as Column<unknown, unknown>;
214
+
215
+ it("renders the dtype label when present", () => {
216
+ renderInMenu(<DataType column={makeColumn("int64")} />);
217
+ expect(screen.getByText("int64")).toBeInTheDocument();
218
+ });
219
+
220
+ it("returns null when dtype is absent", () => {
221
+ renderInMenu(<DataType column={makeColumn()} />);
222
+ expect(screen.queryByText("int64")).toBeNull();
223
+ });
224
+ });
225
+
226
+ describe("Sorts", () => {
227
+ const makeColumn = ({
228
+ canSort = true,
229
+ sorted = false,
230
+ sortIndex = 0,
231
+ }: {
232
+ canSort?: boolean;
233
+ sorted?: false | SortDirection;
234
+ sortIndex?: number;
235
+ } = {}) =>
236
+ ({
237
+ getCanSort: () => canSort,
238
+ getIsSorted: () => sorted,
239
+ getSortIndex: () => sortIndex,
240
+ clearSorting: vi.fn(),
241
+ toggleSorting: vi.fn(),
242
+ }) as unknown as Column<unknown, unknown>;
243
+
244
+ const makeTable = (sorting: SortingState) =>
245
+ ({
246
+ getState: () => ({ sorting }),
247
+ resetSorting: vi.fn(),
248
+ }) as unknown as Table<unknown>;
249
+
250
+ it("returns null when the column cannot sort", () => {
251
+ renderInMenu(<Sorts column={makeColumn({ canSort: false })} />);
252
+ expect(screen.queryByText("Asc")).toBeNull();
253
+ });
254
+
255
+ it("renders Asc and Desc items", () => {
256
+ renderInMenu(<Sorts column={makeColumn()} />);
257
+ expect(screen.getByText("Asc")).toBeInTheDocument();
258
+ expect(screen.getByText("Desc")).toBeInTheDocument();
259
+ });
260
+
261
+ it("offers single-column 'Clear sort' when sorted without multi-sort", () => {
262
+ renderInMenu(<Sorts column={makeColumn({ sorted: "asc" })} />);
263
+ expect(screen.getByText("Clear sort")).toBeInTheDocument();
264
+ });
265
+
266
+ it("offers 'Clear all sorts' when the table has multiple sorts", () => {
267
+ renderInMenu(
268
+ <Sorts
269
+ column={makeColumn({ sorted: "asc" })}
270
+ table={makeTable([
271
+ { id: "a", desc: false },
272
+ { id: "b", desc: true },
273
+ ])}
274
+ />,
275
+ );
276
+ expect(screen.getByText("Clear all sorts")).toBeInTheDocument();
277
+ });
278
+ });
279
+
280
+ describe("CopyColumn", () => {
281
+ const makeColumn = ({
282
+ canCopy = true,
283
+ id = "name",
284
+ }: {
285
+ canCopy?: boolean;
286
+ id?: string;
287
+ } = {}) =>
288
+ ({
289
+ id,
290
+ getCanCopy: () => canCopy,
291
+ }) as unknown as Column<unknown, unknown>;
292
+
293
+ it("renders 'Copy column name' when copyable", () => {
294
+ renderInMenu(<CopyColumn column={makeColumn()} />);
295
+ expect(screen.getByText("Copy column name")).toBeInTheDocument();
296
+ });
297
+
298
+ it("returns null when the column cannot be copied", () => {
299
+ renderInMenu(<CopyColumn column={makeColumn({ canCopy: false })} />);
300
+ expect(screen.queryByText("Copy column name")).toBeNull();
301
+ });
302
+ });
303
+
304
+ describe("ColumnPinning", () => {
305
+ const makeColumn = ({
306
+ canPin = true,
307
+ pinned = false,
308
+ }: {
309
+ canPin?: boolean;
310
+ pinned?: false | "left" | "right";
311
+ } = {}) =>
312
+ ({
313
+ getCanPin: () => canPin,
314
+ getIsPinned: () => pinned,
315
+ pin: vi.fn(),
316
+ }) as unknown as Column<unknown, unknown>;
317
+
318
+ it("returns null when the column cannot be pinned", () => {
319
+ renderInMenu(<ColumnPinning column={makeColumn({ canPin: false })} />);
320
+ expect(screen.queryByText("Freeze left")).toBeNull();
321
+ });
322
+
323
+ it("offers freeze options when unpinned", () => {
324
+ renderInMenu(<ColumnPinning column={makeColumn()} />);
325
+ expect(screen.getByText("Freeze left")).toBeInTheDocument();
326
+ expect(screen.getByText("Freeze right")).toBeInTheDocument();
327
+ });
328
+
329
+ it("offers 'Unfreeze' when pinned", () => {
330
+ renderInMenu(<ColumnPinning column={makeColumn({ pinned: "left" })} />);
331
+ expect(screen.getByText("Unfreeze")).toBeInTheDocument();
332
+ });
333
+ });
334
+
335
+ describe("ColumnWrapping", () => {
336
+ const makeColumn = ({
337
+ canWrap = true,
338
+ wrapping = "nowrap",
339
+ }: {
340
+ canWrap?: boolean;
341
+ wrapping?: "wrap" | "nowrap";
342
+ } = {}) =>
343
+ ({
344
+ getCanWrap: () => canWrap,
345
+ getColumnWrapping: () => wrapping,
346
+ toggleColumnWrapping: vi.fn(),
347
+ }) as unknown as Column<unknown, unknown>;
348
+
349
+ it("returns null when the column cannot wrap", () => {
350
+ renderInMenu(<ColumnWrapping column={makeColumn({ canWrap: false })} />);
351
+ expect(screen.queryByText("Wrap text")).toBeNull();
352
+ });
353
+
354
+ it("offers 'Wrap text' when not wrapping", () => {
355
+ renderInMenu(<ColumnWrapping column={makeColumn()} />);
356
+ expect(screen.getByText("Wrap text")).toBeInTheDocument();
357
+ });
358
+
359
+ it("offers 'No wrap text' when wrapping", () => {
360
+ renderInMenu(<ColumnWrapping column={makeColumn({ wrapping: "wrap" })} />);
361
+ expect(screen.getByText("No wrap text")).toBeInTheDocument();
362
+ });
363
+ });
364
+
365
+ describe("FormatOptions", () => {
366
+ const makeColumn = ({
367
+ dataType = "number",
368
+ canFormat = true,
369
+ }: {
370
+ dataType?: string;
371
+ canFormat?: boolean;
372
+ } = {}) =>
373
+ ({
374
+ columnDef: { meta: { dataType } },
375
+ getCanFormat: () => canFormat,
376
+ getColumnFormatting: () => undefined,
377
+ setColumnFormatting: vi.fn(),
378
+ }) as unknown as Column<unknown, unknown>;
379
+
380
+ it("renders the 'Format' submenu trigger for formattable columns", () => {
381
+ renderInMenu(<FormatOptions column={makeColumn()} locale="en-US" />);
382
+ expect(screen.getByText("Format")).toBeInTheDocument();
383
+ });
384
+
385
+ it("returns null when the column cannot be formatted", () => {
386
+ renderInMenu(
387
+ <FormatOptions
388
+ column={makeColumn({ canFormat: false })}
389
+ locale="en-US"
390
+ />,
391
+ );
392
+ expect(screen.queryByText("Format")).toBeNull();
393
+ });
394
+
395
+ it("returns null when the data type has no format options", () => {
396
+ renderInMenu(
397
+ <FormatOptions
398
+ column={makeColumn({ dataType: "unknown" })}
399
+ locale="en-US"
400
+ />,
401
+ );
402
+ expect(screen.queryByText("Format")).toBeNull();
403
+ });
404
+ });
@@ -1,6 +1,15 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
+ "use no memo";
2
3
 
3
- import { ChevronDownIcon, ChevronRightIcon } from "lucide-react";
4
+ // tanstack/table is not compatible with React compiler
5
+ // https://github.com/TanStack/table/issues/5567
6
+
7
+ import {
8
+ ChevronDownIcon,
9
+ ChevronRightIcon,
10
+ EyeIcon,
11
+ EyeOffIcon,
12
+ } from "lucide-react";
4
13
  import { useState } from "react";
5
14
  import { useLocale } from "react-aria";
6
15
  import {
@@ -38,22 +47,28 @@ import {
38
47
  INDEX_COLUMN_NAME,
39
48
  SELECT_COLUMN_ID,
40
49
  } from "../types";
50
+ import { smartMatch } from "@/utils/smartMatch";
51
+ import type { Column, Table } from "@tanstack/react-table";
52
+ import { cn } from "@/utils/cn";
53
+ import { getColumnCountForDisplay } from "../hooks/use-column-visibility";
41
54
 
42
- interface ColumnExplorerPanelProps {
55
+ interface ColumnExplorerPanelProps<TData> {
43
56
  previewColumn: PreviewColumn;
44
57
  fieldTypes: FieldTypesWithExternalType | undefined | null;
45
58
  totalRows: number | "too_many";
46
59
  totalColumns: number;
47
60
  tableId: string;
61
+ table: Table<TData>;
48
62
  }
49
63
 
50
- export const ColumnExplorerPanel = ({
64
+ export function ColumnExplorerPanel<TData>({
51
65
  previewColumn,
52
66
  fieldTypes,
53
67
  totalRows,
54
68
  totalColumns,
55
69
  tableId,
56
- }: ColumnExplorerPanelProps) => {
70
+ table,
71
+ }: ColumnExplorerPanelProps<TData>) {
57
72
  const [searchValue, setSearchValue] = useState("");
58
73
  const { locale } = useLocale();
59
74
  const columns = fieldTypes?.filter(([columnName]) => {
@@ -68,25 +83,43 @@ export const ColumnExplorerPanel = ({
68
83
  });
69
84
 
70
85
  const filteredColumns = columns?.filter(([columnName]) => {
71
- return columnName.toLowerCase().includes(searchValue.toLowerCase());
86
+ return smartMatch(searchValue, columnName);
72
87
  });
73
88
 
74
- const rowColumnHiddenStr = prettifyRowColumnCount({
89
+ const {
90
+ totalColumns: effectiveTotalColumns,
91
+ hiddenColumns: hiddenColumnCount,
92
+ } = getColumnCountForDisplay(table, totalColumns);
93
+
94
+ const { rowsAndColumns, hiddenSuffix } = prettifyRowColumnCount({
75
95
  numRows: totalRows,
76
- totalColumns,
96
+ totalColumns: effectiveTotalColumns,
77
97
  locale,
78
- }).rowsAndColumns;
98
+ hiddenColumns: hiddenColumnCount,
99
+ });
79
100
 
80
101
  return (
81
102
  <div className="mb-3">
82
- <span className="text-xs font-semibold ml-2 flex">
83
- {rowColumnHiddenStr}
103
+ <div className="text-xs font-semibold ml-2 flex items-center gap-1">
104
+ {rowsAndColumns}
105
+ {hiddenColumnCount > 0 && <span>{hiddenSuffix}</span>}
84
106
  <CopyClipboardIcon
85
107
  tooltip="Copy column names"
86
108
  value={columns?.map(([columnName]) => columnName).join(",\n") || ""}
87
- className="h-3 w-3 ml-1 mt-0.5"
109
+ className="h-3 w-3"
88
110
  />
89
- </span>
111
+ {hiddenColumnCount > 0 && (
112
+ <Button
113
+ type="button"
114
+ variant="link"
115
+ size="xs"
116
+ className="h-auto p-0"
117
+ onClick={() => table.resetColumnVisibility(true)}
118
+ >
119
+ Unhide all
120
+ </Button>
121
+ )}
122
+ </div>
90
123
  <Command className="h-5/6 bg-background" shouldFilter={false}>
91
124
  <CommandInput
92
125
  placeholder="Search columns..."
@@ -97,11 +130,14 @@ export const ColumnExplorerPanel = ({
97
130
  <CommandEmpty>No results.</CommandEmpty>
98
131
  {filteredColumns?.map(
99
132
  ([columnName, [dataType, externalType]], index) => {
133
+ const column = table.getColumn(columnName);
134
+
100
135
  return (
101
136
  <ColumnItem
102
137
  // Tables may have the same column names, hence we use tableId to make it unique
103
138
  key={`${tableId}-${columnName}`}
104
139
  columnName={columnName}
140
+ column={column}
105
141
  dataType={dataType}
106
142
  externalType={externalType}
107
143
  previewColumn={previewColumn}
@@ -114,21 +150,23 @@ export const ColumnExplorerPanel = ({
114
150
  </Command>
115
151
  </div>
116
152
  );
117
- };
153
+ }
118
154
 
119
- const ColumnItem = ({
155
+ function ColumnItem<TData>({
120
156
  columnName,
157
+ column,
121
158
  dataType,
122
159
  externalType,
123
160
  previewColumn,
124
161
  defaultExpanded = false,
125
162
  }: {
126
163
  columnName: string;
164
+ column?: Column<TData, unknown>;
127
165
  dataType: DataType;
128
166
  externalType: string;
129
167
  previewColumn: PreviewColumn;
130
168
  defaultExpanded?: boolean;
131
- }) => {
169
+ }) {
132
170
  const [isExpanded, setIsExpanded] = useState(defaultExpanded);
133
171
 
134
172
  const columnText = (
@@ -148,20 +186,48 @@ const ColumnItem = ({
148
186
  <ChevronRightIcon className="w-3 h-3 shrink-0 text-muted-foreground" />
149
187
  )}
150
188
  <ColumnName columnName={columnText} dataType={dataType} />
151
- <div className="ml-auto">
152
- <Tooltip content="Copy column name" delayDuration={400}>
153
- <Button
154
- variant="text"
155
- size="icon"
156
- className="group-hover:opacity-100 opacity-0 hover:bg-muted text-muted-foreground hover:text-foreground"
189
+ <div className="ml-auto flex items-center gap-0.5">
190
+ <CopyClipboardIcon
191
+ tooltip="Copy column name"
192
+ value={columnName}
193
+ className="h-3 w-3"
194
+ buttonClassName={cn(
195
+ "inline-flex items-center justify-center rounded-md h-6 w-6",
196
+ "group-hover:opacity-100 opacity-0 hover:bg-muted text-muted-foreground hover:text-primary",
197
+ )}
198
+ />
199
+ {column?.getCanHide() && (
200
+ <Tooltip
201
+ content={column.getIsVisible() ? "Hide column" : "Show column"}
202
+ delayDuration={400}
157
203
  >
158
- <CopyClipboardIcon
159
- tooltip={false}
160
- value={columnName}
161
- className="h-3 w-3"
162
- />
163
- </Button>
164
- </Tooltip>
204
+ <Button
205
+ type="button"
206
+ variant="text"
207
+ size="icon"
208
+ aria-label={
209
+ column.getIsVisible() ? "Hide column" : "Show column"
210
+ }
211
+ className={cn(
212
+ "hover:bg-muted text-muted-foreground hover:text-primary",
213
+ column.getIsVisible()
214
+ ? "group-hover:opacity-100 opacity-0"
215
+ : "opacity-100",
216
+ )}
217
+ onClick={(e) => {
218
+ e.preventDefault();
219
+ e.stopPropagation();
220
+ column.toggleVisibility(!column.getIsVisible());
221
+ }}
222
+ >
223
+ {column.getIsVisible() ? (
224
+ <EyeIcon className="h-3 w-3" strokeWidth={2.5} />
225
+ ) : (
226
+ <EyeOffIcon className="h-3 w-3" strokeWidth={2.5} />
227
+ )}
228
+ </Button>
229
+ </Tooltip>
230
+ )}
165
231
  <span className="text-xs text-muted-foreground">{externalType}</span>
166
232
  </div>
167
233
  </CommandItem>
@@ -176,7 +242,7 @@ const ColumnItem = ({
176
242
  )}
177
243
  </>
178
244
  );
179
- };
245
+ }
180
246
 
181
247
  const ColumnPreview = ({
182
248
  previewColumn,
@@ -17,14 +17,14 @@ import { useFilterEditor } from "./filter-editor-context";
17
17
  import { EDITABLE_FILTER_TYPES, isMembershipFilterType } from "./filters";
18
18
  import {
19
19
  ClearFilterMenuItem,
20
+ ColumnPinning,
21
+ ColumnWrapping,
22
+ CopyColumn,
23
+ DataType,
24
+ FormatOptions,
20
25
  HideColumn,
21
- renderColumnPinning,
22
- renderColumnWrapping,
23
- renderCopyColumn,
24
- renderDataType,
25
- renderFormatOptions,
26
26
  renderSortIcon,
27
- renderSorts,
27
+ Sorts,
28
28
  } from "./header-items";
29
29
 
30
30
  interface DataTableColumnHeaderProps<
@@ -36,6 +36,11 @@ interface DataTableColumnHeaderProps<
36
36
  subheader?: React.ReactNode;
37
37
  justify?: "left" | "center" | "right";
38
38
  calculateTopKRows?: CalculateTopKRows;
39
+ /**
40
+ * Optional: only used to surface multi-column sort actions ("Clear all
41
+ * sorts"). Omitted by call sites that define their header inside column
42
+ * definitions, where the table instance isn't yet available.
43
+ */
39
44
  table?: Table<TData>;
40
45
  }
41
46
 
@@ -119,12 +124,12 @@ export const DataTableColumnHeader = <TData, TValue>({
119
124
  </button>
120
125
  </DropdownMenuTrigger>
121
126
  <DropdownMenuContent align="start">
122
- {renderDataType(column)}
123
- {renderSorts(column, table)}
124
- {renderCopyColumn(column)}
125
- {renderColumnPinning(column)}
126
- {renderColumnWrapping(column)}
127
- {renderFormatOptions(column, locale)}
127
+ <DataType column={column} />
128
+ <Sorts column={column} table={table} />
129
+ <CopyColumn column={column} />
130
+ <ColumnPinning column={column} />
131
+ <ColumnWrapping column={column} />
132
+ <FormatOptions column={column} locale={locale} />
128
133
  <HideColumn column={column} />
129
134
  {canEditFilter && <DropdownMenuSeparator />}
130
135
  {canEditFilter && (
@@ -17,6 +17,7 @@ import {
17
17
  type PaginationState,
18
18
  type RowSelectionState,
19
19
  type SortingState,
20
+ type Table as TanstackTable,
20
21
  useReactTable,
21
22
  } from "@tanstack/react-table";
22
23
  import React, { memo } from "react";
@@ -127,6 +128,7 @@ interface DataTableProps<TData> extends Partial<ExportActionProps> {
127
128
  togglePanel?: (panelType: PanelType) => void;
128
129
  isPanelOpen?: (panelType: PanelType) => boolean;
129
130
  isAnyPanelOpen?: boolean;
131
+ renderTableExplorerPanel?: (table: TanstackTable<TData>) => React.ReactNode;
130
132
  }
131
133
 
132
134
  const DataTableInternal = <TData,>({
@@ -178,6 +180,7 @@ const DataTableInternal = <TData,>({
178
180
  isAnyPanelOpen,
179
181
  viewedRowIdx,
180
182
  onViewedRowChange,
183
+ renderTableExplorerPanel,
181
184
  }: DataTableProps<TData>) => {
182
185
  const [showLoadingBar, setShowLoadingBar] = React.useState<boolean>(false);
183
186
  const { locale } = useLocale();
@@ -346,6 +349,7 @@ const DataTableInternal = <TData,>({
346
349
  addFilterSnapshot={addFilterSnapshot}
347
350
  onAddFilterSnapshotChange={setAddFilterSnapshot}
348
351
  />
352
+ {renderTableExplorerPanel?.(table)}
349
353
  <CellSelectionProvider>
350
354
  <div
351
355
  part="table-wrapper"
@@ -9,7 +9,6 @@ import {
9
9
  TableIcon,
10
10
  } from "lucide-react";
11
11
  import React from "react";
12
- import { useLocale } from "react-aria";
13
12
  import { downloadSizeLimitAtom } from "./download-policy/atoms";
14
13
  import { logNever } from "@/utils/assertNever";
15
14
  import { cn } from "@/utils/cn";
@@ -20,7 +19,6 @@ import { Filenames } from "@/utils/filenames";
20
19
  import {
21
20
  jsonParseWithSpecialChar,
22
21
  jsonToMarkdown,
23
- jsonToTSV,
24
22
  } from "@/utils/json/json-parser";
25
23
  import { MissingPackagePrompt } from "../datasources/missing-package-prompt";
26
24
  import { Button } from "../ui/button";
@@ -68,7 +66,12 @@ const FILE_TYPES = {
68
66
  },
69
67
  } as const;
70
68
 
71
- const downloadOptions = [FILE_TYPES.CSV, FILE_TYPES.JSON, FILE_TYPES.PARQUET];
69
+ const downloadOptions = [
70
+ FILE_TYPES.CSV,
71
+ FILE_TYPES.TSV,
72
+ FILE_TYPES.JSON,
73
+ FILE_TYPES.PARQUET,
74
+ ];
72
75
  const copyOptions = [
73
76
  FILE_TYPES.TSV,
74
77
  FILE_TYPES.JSON,
@@ -79,6 +82,15 @@ const copyOptions = [
79
82
  type DownloadFormat = (typeof downloadOptions)[number]["format"];
80
83
  type CopyFormat = (typeof copyOptions)[number]["format"];
81
84
 
85
+ // Each clipboard-copy format fetches from a backend download format, then
86
+ // transforms the payload client-side as needed.
87
+ const COPY_SOURCE_FORMAT: Record<CopyFormat, DownloadFormat> = {
88
+ csv: "csv",
89
+ tsv: "tsv",
90
+ json: "json",
91
+ markdown: "json",
92
+ };
93
+
82
94
  export interface ExportActionProps {
83
95
  downloadAs: (req: { format: DownloadFormat }) => Promise<{
84
96
  url: string;
@@ -100,7 +112,6 @@ const labelForCopyFormat = (format: CopyFormat): string =>
100
112
  copyOptions.find((opt) => opt.format === format)?.label ?? format;
101
113
 
102
114
  export const ExportMenu: React.FC<ExportActionProps> = (props) => {
103
- const { locale } = useLocale();
104
115
  const [downloadMenuOpen, setDownloadMenuOpen] = React.useState(false);
105
116
  const policy = useAtomValue(downloadSizeLimitAtom);
106
117
  const overLimit = !!(
@@ -213,7 +224,7 @@ export const ExportMenu: React.FC<ExportActionProps> = (props) => {
213
224
  await withLoadingToast(
214
225
  `Preparing ${labelForCopyFormat(format)} for clipboard...`,
215
226
  async () => {
216
- const sourceFormat: DownloadFormat = format === "csv" ? "csv" : "json";
227
+ const sourceFormat = COPY_SOURCE_FORMAT[format];
217
228
  const result = await resolveDownloadUrl(sourceFormat, () => {
218
229
  void handleClipboardCopy(format);
219
230
  });
@@ -223,19 +234,15 @@ export const ExportMenu: React.FC<ExportActionProps> = (props) => {
223
234
 
224
235
  let text: string;
225
236
  switch (format) {
226
- case "tsv": {
227
- const json = await fetchJson(result.url);
228
- text = jsonToTSV(json, locale);
237
+ case "tsv":
238
+ case "csv":
239
+ text = await fetchText(result.url);
229
240
  break;
230
- }
231
241
  case "json": {
232
242
  const json = await fetchJson(result.url);
233
243
  text = JSON.stringify(json, null, 2);
234
244
  break;
235
245
  }
236
- case "csv":
237
- text = await fetchText(result.url);
238
- break;
239
246
  case "markdown": {
240
247
  const json = await fetchJson(result.url);
241
248
  text = jsonToMarkdown(json);