@marimo-team/islands 0.23.9-dev4 → 0.23.9-dev41

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 (97) 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-VZebNmSs.js → code-visibility-s3OWNKLN.js} +1634 -1314
  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 +1522 -1556
  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-DZtPMEoM.js → reveal-component-DCO6zm_5.js} +17 -17
  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 +27 -6
  31. package/src/components/data-table/TableTopBar.tsx +7 -1
  32. package/src/components/data-table/__tests__/TableBottomBar.test.tsx +73 -0
  33. package/src/components/data-table/__tests__/column-explorer.test.tsx +128 -0
  34. package/src/components/data-table/__tests__/data-table.test.tsx +52 -1
  35. package/src/components/data-table/__tests__/header-items.test.tsx +257 -1
  36. package/src/components/data-table/__tests__/useColumnVisibility.test.ts +42 -0
  37. package/src/components/data-table/column-explorer-panel/column-explorer.tsx +98 -26
  38. package/src/components/data-table/column-header.tsx +19 -12
  39. package/src/components/data-table/columns.tsx +3 -4
  40. package/src/components/data-table/data-table.tsx +37 -0
  41. package/src/components/data-table/export-actions.tsx +36 -18
  42. package/src/components/data-table/header-items.tsx +58 -17
  43. package/src/components/data-table/hooks/use-column-visibility.ts +56 -0
  44. package/src/components/data-table/pagination.tsx +16 -3
  45. package/src/components/data-table/schemas.ts +2 -2
  46. package/src/components/data-table/table-explorer-panel/table-explorer-panel.tsx +16 -6
  47. package/src/components/databases/display.tsx +2 -0
  48. package/src/components/datasources/__tests__/utils.test.ts +82 -0
  49. package/src/components/datasources/utils.ts +16 -15
  50. package/src/components/editor/actions/pair-with-agent-modal.tsx +1 -0
  51. package/src/components/editor/actions/useCellActionButton.tsx +3 -3
  52. package/src/components/editor/actions/useNotebookActions.tsx +5 -2
  53. package/src/components/editor/cell/code/cell-editor.tsx +7 -4
  54. package/src/components/editor/chrome/types.ts +13 -6
  55. package/src/components/editor/chrome/wrapper/app-chrome.tsx +6 -4
  56. package/src/components/editor/chrome/wrapper/footer-items/ai-status.tsx +10 -1
  57. package/src/components/editor/chrome/wrapper/sidebar.tsx +7 -5
  58. package/src/components/editor/errors/auto-fix.tsx +3 -3
  59. package/src/components/editor/errors/mangled-local-chip.tsx +50 -0
  60. package/src/components/editor/navigation/__tests__/navigation.test.ts +15 -0
  61. package/src/components/editor/navigation/navigation.ts +5 -0
  62. package/src/components/editor/output/MarimoErrorOutput.tsx +110 -27
  63. package/src/components/editor/output/MarimoTracebackOutput.tsx +55 -37
  64. package/src/components/editor/renderers/cell-array.tsx +27 -24
  65. package/src/components/editor/renderers/slides-layout/slides-layout.tsx +1 -1
  66. package/src/components/slides/reveal-component.tsx +3 -3
  67. package/src/components/slides/slide-form.tsx +11 -3
  68. package/src/components/static-html/static-banner.tsx +28 -22
  69. package/src/core/ai/__tests__/model-registry.test.ts +72 -60
  70. package/src/core/ai/model-registry.ts +33 -28
  71. package/src/core/cells/__tests__/actions.test.ts +48 -0
  72. package/src/core/cells/actions.ts +5 -6
  73. package/src/core/codemirror/__tests__/setup.test.ts +29 -0
  74. package/src/core/codemirror/cells/traceback-decorations.ts +1 -1
  75. package/src/core/codemirror/cm.ts +3 -2
  76. package/src/core/codemirror/completion/hints.ts +4 -1
  77. package/src/core/codemirror/format.ts +1 -0
  78. package/src/core/codemirror/keymaps/vim.ts +63 -0
  79. package/src/core/codemirror/language/languages/sql/sql.ts +1 -0
  80. package/src/core/codemirror/language/languages/sql/utils.ts +2 -0
  81. package/src/core/config/__tests__/config-schema.test.ts +4 -0
  82. package/src/core/config/config-schema.ts +4 -0
  83. package/src/core/config/config.ts +16 -0
  84. package/src/css/app/Cell.css +0 -1
  85. package/src/plugins/impl/DataTablePlugin.tsx +94 -33
  86. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +1 -0
  87. package/src/plugins/impl/chat/ChatPlugin.tsx +7 -1
  88. package/src/plugins/impl/chat/__tests__/chat-ui.test.ts +278 -0
  89. package/src/plugins/impl/chat/chat-ui.tsx +106 -59
  90. package/src/plugins/impl/chat/types.ts +5 -0
  91. package/src/plugins/impl/data-frames/DataFramePlugin.tsx +8 -6
  92. package/src/stories/dataframe.stories.tsx +1 -0
  93. package/src/utils/__tests__/json-parser.test.ts +1 -69
  94. package/src/utils/__tests__/local-variables.test.ts +132 -0
  95. package/src/utils/json/json-parser.ts +0 -30
  96. package/src/utils/local-variables.ts +67 -0
  97. package/dist/assets/__vite-browser-external-CAdMKBac.js +0 -1
@@ -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,19 +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);
87
+ });
88
+
89
+ const {
90
+ totalColumns: effectiveTotalColumns,
91
+ hiddenColumns: hiddenColumnCount,
92
+ } = getColumnCountForDisplay(table, totalColumns);
93
+
94
+ const { rowsAndColumns, hiddenSuffix } = prettifyRowColumnCount({
95
+ numRows: totalRows,
96
+ totalColumns: effectiveTotalColumns,
97
+ locale,
98
+ hiddenColumns: hiddenColumnCount,
72
99
  });
73
100
 
74
101
  return (
75
102
  <div className="mb-3">
76
- <span className="text-xs font-semibold ml-2 flex">
77
- {prettifyRowColumnCount({ numRows: totalRows, totalColumns, locale })}
103
+ <div className="text-xs font-semibold ml-2 flex items-center gap-1">
104
+ {rowsAndColumns}
105
+ {hiddenColumnCount > 0 && <span>{hiddenSuffix}</span>}
78
106
  <CopyClipboardIcon
79
107
  tooltip="Copy column names"
80
108
  value={columns?.map(([columnName]) => columnName).join(",\n") || ""}
81
- className="h-3 w-3 ml-1 mt-0.5"
109
+ className="h-3 w-3"
82
110
  />
83
- </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>
84
123
  <Command className="h-5/6 bg-background" shouldFilter={false}>
85
124
  <CommandInput
86
125
  placeholder="Search columns..."
@@ -91,11 +130,14 @@ export const ColumnExplorerPanel = ({
91
130
  <CommandEmpty>No results.</CommandEmpty>
92
131
  {filteredColumns?.map(
93
132
  ([columnName, [dataType, externalType]], index) => {
133
+ const column = table.getColumn(columnName);
134
+
94
135
  return (
95
136
  <ColumnItem
96
137
  // Tables may have the same column names, hence we use tableId to make it unique
97
138
  key={`${tableId}-${columnName}`}
98
139
  columnName={columnName}
140
+ column={column}
99
141
  dataType={dataType}
100
142
  externalType={externalType}
101
143
  previewColumn={previewColumn}
@@ -108,21 +150,23 @@ export const ColumnExplorerPanel = ({
108
150
  </Command>
109
151
  </div>
110
152
  );
111
- };
153
+ }
112
154
 
113
- const ColumnItem = ({
155
+ function ColumnItem<TData>({
114
156
  columnName,
157
+ column,
115
158
  dataType,
116
159
  externalType,
117
160
  previewColumn,
118
161
  defaultExpanded = false,
119
162
  }: {
120
163
  columnName: string;
164
+ column?: Column<TData, unknown>;
121
165
  dataType: DataType;
122
166
  externalType: string;
123
167
  previewColumn: PreviewColumn;
124
168
  defaultExpanded?: boolean;
125
- }) => {
169
+ }) {
126
170
  const [isExpanded, setIsExpanded] = useState(defaultExpanded);
127
171
 
128
172
  const columnText = (
@@ -142,20 +186,48 @@ const ColumnItem = ({
142
186
  <ChevronRightIcon className="w-3 h-3 shrink-0 text-muted-foreground" />
143
187
  )}
144
188
  <ColumnName columnName={columnText} dataType={dataType} />
145
- <div className="ml-auto">
146
- <Tooltip content="Copy column name" delayDuration={400}>
147
- <Button
148
- variant="text"
149
- size="icon"
150
- 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}
151
203
  >
152
- <CopyClipboardIcon
153
- tooltip={false}
154
- value={columnName}
155
- className="h-3 w-3"
156
- />
157
- </Button>
158
- </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
+ )}
159
231
  <span className="text-xs text-muted-foreground">{externalType}</span>
160
232
  </div>
161
233
  </CommandItem>
@@ -170,7 +242,7 @@ const ColumnItem = ({
170
242
  )}
171
243
  </>
172
244
  );
173
- };
245
+ }
174
246
 
175
247
  const ColumnPreview = ({
176
248
  previewColumn,
@@ -17,13 +17,14 @@ import { useFilterEditor } from "./filter-editor-context";
17
17
  import { EDITABLE_FILTER_TYPES, isMembershipFilterType } from "./filters";
18
18
  import {
19
19
  ClearFilterMenuItem,
20
- renderColumnPinning,
21
- renderColumnWrapping,
22
- renderCopyColumn,
23
- renderDataType,
24
- renderFormatOptions,
20
+ ColumnPinning,
21
+ ColumnWrapping,
22
+ CopyColumn,
23
+ DataType,
24
+ FormatOptions,
25
+ HideColumn,
25
26
  renderSortIcon,
26
- renderSorts,
27
+ Sorts,
27
28
  } from "./header-items";
28
29
 
29
30
  interface DataTableColumnHeaderProps<
@@ -35,6 +36,11 @@ interface DataTableColumnHeaderProps<
35
36
  subheader?: React.ReactNode;
36
37
  justify?: "left" | "center" | "right";
37
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
+ */
38
44
  table?: Table<TData>;
39
45
  }
40
46
 
@@ -118,12 +124,13 @@ export const DataTableColumnHeader = <TData, TValue>({
118
124
  </button>
119
125
  </DropdownMenuTrigger>
120
126
  <DropdownMenuContent align="start">
121
- {renderDataType(column)}
122
- {renderSorts(column, table)}
123
- {renderCopyColumn(column)}
124
- {renderColumnPinning(column)}
125
- {renderColumnWrapping(column)}
126
- {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} />
133
+ <HideColumn column={column} />
127
134
  {canEditFilter && <DropdownMenuSeparator />}
128
135
  {canEditFilter && (
129
136
  <DropdownMenuItem
@@ -44,9 +44,10 @@ import { detectSentinel, splitLeadingTrailingWhitespace } from "./utils";
44
44
  import { uniformSample } from "./uniformSample";
45
45
  import { MarkdownUrlDetector, UrlDetector } from "./url-detector";
46
46
 
47
+ export const NAMELESS_COLUMN_PREFIX = "__m_column__";
47
48
  // Artificial limit to display long strings
49
+ export const SELECT_ID = "__select__";
48
50
  const MAX_STRING_LENGTH = 50;
49
- const SELECT_ID = "__select__";
50
51
 
51
52
  function inferDataType(value: unknown): [type: DataType, displayType: string] {
52
53
  if (typeof value === "string") {
@@ -106,8 +107,6 @@ export function inferFieldTypes<T>(items: T[]): FieldTypesWithExternalType {
106
107
  return Objects.entries(fieldTypes);
107
108
  }
108
109
 
109
- export const NAMELESS_COLUMN_PREFIX = "__m_column__";
110
-
111
110
  export function generateColumns<T>({
112
111
  rowHeaders,
113
112
  selection,
@@ -192,7 +191,7 @@ export function generateColumns<T>({
192
191
  accessorFn: (row) => {
193
192
  return row[key as keyof T];
194
193
  },
195
-
194
+ enableHiding: !rowHeadersSet.has(key) && key !== "",
196
195
  header: ({ column, table }) => {
197
196
  const stats = chartSpecModel?.getColumnStats(key);
198
197
  const dtype = column.columnDef.meta?.dtype;
@@ -17,12 +17,15 @@ 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";
23
24
  import { useLocale } from "react-aria";
24
25
 
26
+ import { Button } from "@/components/ui/button";
25
27
  import { Table } from "@/components/ui/table";
28
+ import { Banner } from "@/plugins/impl/common/error-banner";
26
29
  import type {
27
30
  CalculateTopKRows,
28
31
  GetRowIds,
@@ -63,6 +66,10 @@ import {
63
66
  type TooManyRows,
64
67
  } from "./types";
65
68
  import { getStableRowId } from "./utils";
69
+ import {
70
+ getUserColumnVisibilityCounts,
71
+ useColumnVisibility,
72
+ } from "./hooks/use-column-visibility";
66
73
 
67
74
  interface DataTableProps<TData> extends Partial<ExportActionProps> {
68
75
  wrapperClassName?: string;
@@ -80,6 +87,7 @@ interface DataTableProps<TData> extends Partial<ExportActionProps> {
80
87
  // JSON-serialized size of the currently-rendered data. Forwarded to
81
88
  // ExportMenu so hosts can size-gate the Export button via downloadSizeLimitAtom.
82
89
  sizeBytes?: number | null;
90
+ sizeBytesIsLoading?: boolean;
83
91
  totalColumns: number;
84
92
  pagination?: boolean;
85
93
  manualPagination?: boolean; // server-side pagination
@@ -107,6 +115,7 @@ interface DataTableProps<TData> extends Partial<ExportActionProps> {
107
115
  // Columns
108
116
  freezeColumnsLeft?: string[];
109
117
  freezeColumnsRight?: string[];
118
+ hiddenColumns?: string[];
110
119
  toggleDisplayHeader?: () => void;
111
120
  // Row viewer panel
112
121
  viewedRowIdx?: number;
@@ -119,6 +128,7 @@ interface DataTableProps<TData> extends Partial<ExportActionProps> {
119
128
  togglePanel?: (panelType: PanelType) => void;
120
129
  isPanelOpen?: (panelType: PanelType) => boolean;
121
130
  isAnyPanelOpen?: boolean;
131
+ renderTableExplorerPanel?: (table: TanstackTable<TData>) => React.ReactNode;
122
132
  }
123
133
 
124
134
  const DataTableInternal = <TData,>({
@@ -132,6 +142,7 @@ const DataTableInternal = <TData,>({
132
142
  totalColumns,
133
143
  totalRows,
134
144
  sizeBytes,
145
+ sizeBytesIsLoading,
135
146
  manualSorting = false,
136
147
  sorting,
137
148
  setSorting,
@@ -158,6 +169,7 @@ const DataTableInternal = <TData,>({
158
169
  reloading,
159
170
  freezeColumnsLeft,
160
171
  freezeColumnsRight,
172
+ hiddenColumns,
161
173
  toggleDisplayHeader,
162
174
  showChartBuilder,
163
175
  isChartBuilderOpen,
@@ -168,6 +180,7 @@ const DataTableInternal = <TData,>({
168
180
  isAnyPanelOpen,
169
181
  viewedRowIdx,
170
182
  onViewedRowChange,
183
+ renderTableExplorerPanel,
171
184
  }: DataTableProps<TData>) => {
172
185
  const [showLoadingBar, setShowLoadingBar] = React.useState<boolean>(false);
173
186
  const { locale } = useLocale();
@@ -176,6 +189,8 @@ const DataTableInternal = <TData,>({
176
189
  freezeColumnsLeft,
177
190
  freezeColumnsRight,
178
191
  );
192
+ const { columnVisibility, setColumnVisibility } =
193
+ useColumnVisibility(hiddenColumns);
179
194
 
180
195
  // Show loading bar only after a short delay to prevent flickering
181
196
  React.useEffect(() => {
@@ -267,6 +282,8 @@ const DataTableInternal = <TData,>({
267
282
  enableMultiCellSelection: selection === "multi-cell",
268
283
  // pinning
269
284
  onColumnPinningChange: setColumnPinning,
285
+ // col visibility
286
+ onColumnVisibilityChange: setColumnVisibility,
270
287
  // focus row
271
288
  enableFocusRow: true,
272
289
  onFocusRowChange: onViewedRowChange,
@@ -284,6 +301,7 @@ const DataTableInternal = <TData,>({
284
301
  { pagination: { pageIndex: 0, pageSize: data.length } }),
285
302
  rowSelection: rowSelection ?? {},
286
303
  cellSelection: cellSelection ?? [],
304
+ columnVisibility,
287
305
  cellStyling,
288
306
  columnPinning: columnPinning,
289
307
  cellHoverTemplate: hoverTemplate,
@@ -317,6 +335,10 @@ const DataTableInternal = <TData,>({
317
335
  [table],
318
336
  );
319
337
 
338
+ const visibilityCounts = getUserColumnVisibilityCounts(table);
339
+ const allUserColumnsHidden =
340
+ visibilityCounts.total > 0 && visibilityCounts.visible === 0;
341
+
320
342
  return (
321
343
  <FilterEditorProvider value={filterEditor}>
322
344
  <div className={cn(wrapperClassName, "flex flex-col space-y-1")}>
@@ -327,6 +349,7 @@ const DataTableInternal = <TData,>({
327
349
  addFilterSnapshot={addFilterSnapshot}
328
350
  onAddFilterSnapshotChange={setAddFilterSnapshot}
329
351
  />
352
+ {renderTableExplorerPanel?.(table)}
330
353
  <CellSelectionProvider>
331
354
  <div
332
355
  part="table-wrapper"
@@ -345,7 +368,20 @@ const DataTableInternal = <TData,>({
345
368
  isAnyPanelOpen={isAnyPanelOpen}
346
369
  downloadAs={downloadAs}
347
370
  sizeBytes={sizeBytes}
371
+ sizeBytesIsLoading={sizeBytesIsLoading}
348
372
  />
373
+ {allUserColumnsHidden && (
374
+ <Banner className="mb-1 mx-2 rounded flex items-center justify-between">
375
+ <span>All columns are hidden.</span>
376
+ <Button
377
+ variant="link"
378
+ size="xs"
379
+ onClick={() => table.resetColumnVisibility(true)}
380
+ >
381
+ Unhide all
382
+ </Button>
383
+ </Banner>
384
+ )}
349
385
  <Table
350
386
  className={cn(
351
387
  "relative",
@@ -377,6 +413,7 @@ const DataTableInternal = <TData,>({
377
413
  getRowIds={getRowIds}
378
414
  showPageSizeSelector={showPageSizeSelector}
379
415
  tableLoading={reloading}
416
+ togglePanel={togglePanel}
380
417
  />
381
418
  </div>
382
419
  </CellSelectionProvider>
@@ -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;
@@ -91,6 +103,7 @@ export interface ExportActionProps {
91
103
  // marimo-lsp inside VS Code) declares a download size cap. Null/undefined
92
104
  // means "no info" and the gate stays disabled (fail-open).
93
105
  sizeBytes?: number | null;
106
+ sizeBytesIsLoading?: boolean;
94
107
  }
95
108
 
96
109
  const labelForDownloadFormat = (format: DownloadFormat): string =>
@@ -99,14 +112,19 @@ const labelForCopyFormat = (format: CopyFormat): string =>
99
112
  copyOptions.find((opt) => opt.format === format)?.label ?? format;
100
113
 
101
114
  export const ExportMenu: React.FC<ExportActionProps> = (props) => {
102
- const { locale } = useLocale();
103
- const [open, setOpen] = React.useState(false);
115
+ const [downloadMenuOpen, setDownloadMenuOpen] = React.useState(false);
104
116
  const policy = useAtomValue(downloadSizeLimitAtom);
105
- const disabled = !!(
117
+ const overLimit = !!(
106
118
  policy &&
107
119
  props.sizeBytes != null &&
108
120
  props.sizeBytes > policy.limitBytes
109
121
  );
122
+ const disabled = !!(policy && (props.sizeBytesIsLoading || overLimit));
123
+ const tooltipContent = !disabled
124
+ ? "Export"
125
+ : props.sizeBytesIsLoading
126
+ ? "Checking download size…"
127
+ : policy?.unavailableMessage;
110
128
 
111
129
  const button = (
112
130
  <Button
@@ -116,7 +134,7 @@ export const ExportMenu: React.FC<ExportActionProps> = (props) => {
116
134
  disabled={disabled}
117
135
  className={cn(
118
136
  "print:hidden text-xs gap-1",
119
- open ? "text-primary" : "text-muted-foreground",
137
+ downloadMenuOpen ? "text-primary" : "text-muted-foreground",
120
138
  )}
121
139
  >
122
140
  <DownloadIcon className="w-3.5 h-3.5" />
@@ -206,7 +224,7 @@ export const ExportMenu: React.FC<ExportActionProps> = (props) => {
206
224
  await withLoadingToast(
207
225
  `Preparing ${labelForCopyFormat(format)} for clipboard...`,
208
226
  async () => {
209
- const sourceFormat: DownloadFormat = format === "csv" ? "csv" : "json";
227
+ const sourceFormat = COPY_SOURCE_FORMAT[format];
210
228
  const result = await resolveDownloadUrl(sourceFormat, () => {
211
229
  void handleClipboardCopy(format);
212
230
  });
@@ -216,19 +234,15 @@ export const ExportMenu: React.FC<ExportActionProps> = (props) => {
216
234
 
217
235
  let text: string;
218
236
  switch (format) {
219
- case "tsv": {
220
- const json = await fetchJson(result.url);
221
- text = jsonToTSV(json, locale);
237
+ case "tsv":
238
+ case "csv":
239
+ text = await fetchText(result.url);
222
240
  break;
223
- }
224
241
  case "json": {
225
242
  const json = await fetchJson(result.url);
226
243
  text = JSON.stringify(json, null, 2);
227
244
  break;
228
245
  }
229
- case "csv":
230
- text = await fetchText(result.url);
231
- break;
232
246
  case "markdown": {
233
247
  const json = await fetchJson(result.url);
234
248
  text = jsonToMarkdown(json);
@@ -248,10 +262,14 @@ export const ExportMenu: React.FC<ExportActionProps> = (props) => {
248
262
  };
249
263
 
250
264
  return (
251
- <DropdownMenu modal={false} open={open} onOpenChange={setOpen}>
265
+ <DropdownMenu
266
+ modal={false}
267
+ open={downloadMenuOpen}
268
+ onOpenChange={setDownloadMenuOpen}
269
+ >
252
270
  <Tooltip
253
- content={disabled ? policy?.unavailableMessage : "Export"}
254
- open={open ? false : undefined}
271
+ content={tooltipContent}
272
+ open={downloadMenuOpen ? false : undefined}
255
273
  >
256
274
  <DropdownMenuTrigger asChild={true} disabled={disabled}>
257
275
  <span tabIndex={disabled ? 0 : -1} className="inline-flex">