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

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.
@@ -9,7 +9,7 @@ import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
9
9
  import { ct as kioskModeAtom } from "./html-to-image-CiSinpSR.js";
10
10
  import "./chunk-5FQGJX7Z-BNjes6Yx.js";
11
11
  import { u as createLucideIcon } from "./dist-C1BYNeCR.js";
12
- import { G as PanelGroup, Gt as Code, Ht as Expand, K as PanelResizeHandle, Vt as EyeOff, W as Panel, a as DEFAULT_SLIDE_TYPE, c as Slide, i as DEFAULT_DECK_TRANSITION, s as SlideSidebar, t as useNotebookCodeAvailable } from "./code-visibility-VZebNmSs.js";
12
+ import { G as PanelGroup, Gt as Code, Ht as Expand, K as PanelResizeHandle, Vt as EyeOff, W as Panel, a as DEFAULT_SLIDE_TYPE, c as Slide, i as DEFAULT_DECK_TRANSITION, s as SlideSidebar, t as useNotebookCodeAvailable } from "./code-visibility-DILs4Gug.js";
13
13
  import { q as useDebouncedCallback } from "./input-CZD2z6X2.js";
14
14
  import "./toDate-ZVVIBmdk.js";
15
15
  import "./react-dom-BTJzcVJ9.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.23.9-dev4",
3
+ "version": "0.23.9-dev6",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -7,8 +7,13 @@ import type { GetRowIds } from "@/plugins/impl/DataTablePlugin";
7
7
  import { cn } from "@/utils/cn";
8
8
  import { Events } from "@/utils/events";
9
9
  import { prettyNumber } from "@/utils/numbers";
10
+ import {
11
+ PANEL_TYPES,
12
+ type PanelType,
13
+ } from "../editor/chrome/panels/context-aware-panel/context-aware-panel";
10
14
  import { Button } from "../ui/button";
11
15
  import { toast } from "../ui/use-toast";
16
+ import { getUserColumnVisibilityCounts } from "./hooks/use-column-visibility";
12
17
  import { DataTablePagination, prettifyRowColumnCount } from "./pagination";
13
18
  import { CellSelectionStats } from "./range-focus/cell-selection-stats";
14
19
  import type { DataTableSelection } from "./types";
@@ -22,6 +27,7 @@ interface TableBottomBarProps<TData> {
22
27
  getRowIds?: GetRowIds;
23
28
  showPageSizeSelector?: boolean;
24
29
  tableLoading?: boolean;
30
+ togglePanel?: (panelType: PanelType) => void;
25
31
  part?: string;
26
32
  className?: string;
27
33
  }
@@ -35,6 +41,7 @@ export const TableBottomBar = <TData,>({
35
41
  getRowIds,
36
42
  showPageSizeSelector,
37
43
  tableLoading,
44
+ togglePanel,
38
45
  part,
39
46
  className,
40
47
  }: TableBottomBarProps<TData>) => {
@@ -140,13 +147,30 @@ export const TableBottomBar = <TData,>({
140
147
  );
141
148
  }
142
149
 
150
+ const counts = getUserColumnVisibilityCounts(table);
151
+ // When columns are clipped, the table instance only has the rendered
152
+ // subset, so the visible/hidden math must use that subset's total. The
153
+ // dataset-wide `totalColumns` prop is only correct for the no-hidden
154
+ // "N columns" label.
155
+ const { rowsAndColumns, hiddenSuffix } = prettifyRowColumnCount({
156
+ numRows: table.getRowCount(),
157
+ totalColumns: counts.hidden > 0 ? counts.total : totalColumns,
158
+ hiddenColumns: counts.hidden,
159
+ locale,
160
+ });
161
+
143
162
  return (
144
- <span>
145
- {prettifyRowColumnCount({
146
- numRows: table.getRowCount(),
147
- totalColumns,
148
- locale,
149
- })}
163
+ <span className="flex items-center gap-1">
164
+ <span>{rowsAndColumns}</span>
165
+ {hiddenSuffix && (
166
+ <button
167
+ type="button"
168
+ className="text-xs underline-offset-2 hover:underline cursor-pointer"
169
+ onClick={() => togglePanel?.(PANEL_TYPES.COLUMN_EXPLORER)}
170
+ >
171
+ {hiddenSuffix}
172
+ </button>
173
+ )}
150
174
  </span>
151
175
  );
152
176
  };
@@ -0,0 +1,73 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ "use no memo";
3
+
4
+ import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
5
+ import { render, screen } from "@testing-library/react";
6
+ import { describe, expect, it, vi } from "vitest";
7
+ import { TooltipProvider } from "@/components/ui/tooltip";
8
+ import { CellSelectionProvider } from "../range-focus/provider";
9
+ import { TableBottomBar } from "../TableBottomBar";
10
+
11
+ function renderWithTable(opts: {
12
+ totalColumns: number;
13
+ hiddenColumns?: string[];
14
+ togglePanel?: (panelType: string) => void;
15
+ }) {
16
+ const Wrapper = () => {
17
+ const table = useReactTable({
18
+ data: [] as Array<Record<string, unknown>>,
19
+ columns: Array.from({ length: opts.totalColumns }, (_, i) => ({
20
+ id: `col${i}`,
21
+ enableHiding: true,
22
+ })),
23
+ getCoreRowModel: getCoreRowModel(),
24
+ locale: "en-US",
25
+ state: {
26
+ columnVisibility: Object.fromEntries(
27
+ (opts.hiddenColumns ?? []).map((c) => [c, false]),
28
+ ),
29
+ },
30
+ });
31
+
32
+ return (
33
+ <TableBottomBar
34
+ pagination={false}
35
+ totalColumns={opts.totalColumns}
36
+ table={table}
37
+ togglePanel={opts.togglePanel}
38
+ />
39
+ );
40
+ };
41
+
42
+ return render(
43
+ <TooltipProvider>
44
+ <CellSelectionProvider>
45
+ <Wrapper />
46
+ </CellSelectionProvider>
47
+ </TooltipProvider>,
48
+ );
49
+ }
50
+
51
+ describe("TableBottomBar — hidden column count", () => {
52
+ it("does not render '(n hidden)' when no columns are hidden", () => {
53
+ renderWithTable({ totalColumns: 3 });
54
+ expect(screen.queryByText(/hidden/)).toBeNull();
55
+ });
56
+
57
+ it("renders 'X visible (n hidden)' when columns are hidden", () => {
58
+ renderWithTable({ totalColumns: 3, hiddenColumns: ["col1"] });
59
+ expect(screen.getByText(/2 visible/)).toBeInTheDocument();
60
+ expect(screen.getByText(/\(1 hidden\)/)).toBeInTheDocument();
61
+ });
62
+
63
+ it("invokes togglePanel('column-explorer') when '(n hidden)' is clicked", () => {
64
+ const togglePanel = vi.fn();
65
+ renderWithTable({
66
+ totalColumns: 3,
67
+ hiddenColumns: ["col1"],
68
+ togglePanel,
69
+ });
70
+ screen.getByText(/\(1 hidden\)/).click();
71
+ expect(togglePanel).toHaveBeenCalledWith("column-explorer");
72
+ });
73
+ });
@@ -5,7 +5,7 @@ import type {
5
5
  RowSelectionState,
6
6
  SortingState,
7
7
  } from "@tanstack/react-table";
8
- import { render, screen, within } from "@testing-library/react";
8
+ import { fireEvent, render, screen, within } from "@testing-library/react";
9
9
  import { describe, expect, it, vi } from "vitest";
10
10
  import { TooltipProvider } from "@/components/ui/tooltip";
11
11
  import { DataTable } from "../data-table";
@@ -251,3 +251,54 @@ describe("DataTable", () => {
251
251
  expect(within(updatedRows[3]).getByText("pending")).toBeTruthy();
252
252
  });
253
253
  });
254
+
255
+ describe("DataTable — all-hidden banner", () => {
256
+ interface Row {
257
+ a: number;
258
+ b: number;
259
+ }
260
+
261
+ const columns: ColumnDef<Row>[] = [
262
+ { accessorKey: "a", header: "A" },
263
+ { accessorKey: "b", header: "B" },
264
+ ];
265
+ const data: Row[] = [{ a: 1, b: 2 }];
266
+
267
+ const renderWithVisibility = (hiddenColumns: string[]) =>
268
+ render(
269
+ <TooltipProvider>
270
+ <DataTable
271
+ data={data}
272
+ columns={columns}
273
+ selection={null}
274
+ totalRows={1}
275
+ totalColumns={2}
276
+ pagination={false}
277
+ hiddenColumns={hiddenColumns}
278
+ />
279
+ </TooltipProvider>,
280
+ );
281
+
282
+ it("renders banner when every user column is hidden", () => {
283
+ renderWithVisibility(["a", "b"]);
284
+ expect(screen.getByText(/All columns are hidden/i)).toBeInTheDocument();
285
+ expect(screen.getByText(/Unhide all/i)).toBeInTheDocument();
286
+ });
287
+
288
+ it("does not render the banner when at least one column is visible", () => {
289
+ renderWithVisibility(["a"]);
290
+ expect(screen.queryByText(/All columns are hidden/i)).toBeNull();
291
+ });
292
+
293
+ it("does not render the banner when no columns are hidden", () => {
294
+ renderWithVisibility([]);
295
+ expect(screen.queryByText(/All columns are hidden/i)).toBeNull();
296
+ });
297
+
298
+ it("'Unhide all' restores columns hidden via the Python kwarg", () => {
299
+ renderWithVisibility(["a", "b"]);
300
+ expect(screen.getByText(/All columns are hidden/i)).toBeInTheDocument();
301
+ fireEvent.click(screen.getByText(/Unhide all/i));
302
+ expect(screen.queryByText(/All columns are hidden/i)).toBeNull();
303
+ });
304
+ });
@@ -1,7 +1,14 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import type { SortingState } from "@tanstack/react-table";
3
+ import type { Column, SortingState } from "@tanstack/react-table";
4
+ import { fireEvent, render, screen } from "@testing-library/react";
4
5
  import { describe, expect, it, vi } from "vitest";
6
+ import {
7
+ DropdownMenu,
8
+ DropdownMenuContent,
9
+ DropdownMenuTrigger,
10
+ } from "@/components/ui/dropdown-menu";
11
+ import { HideColumn } from "../header-items";
5
12
 
6
13
  describe("multi-column sorting logic", () => {
7
14
  // Extract the core sorting logic to test in isolation
@@ -146,3 +153,42 @@ describe("multi-column sorting logic", () => {
146
153
  // After removal, dept should move from priority 3 to priority 2
147
154
  });
148
155
  });
156
+
157
+ describe("HideColumn", () => {
158
+ const makeColumn = ({
159
+ canHide = true,
160
+ toggleVisibility = vi.fn(),
161
+ }: {
162
+ canHide?: boolean;
163
+ toggleVisibility?: (value?: boolean) => void;
164
+ } = {}) =>
165
+ ({
166
+ getCanHide: () => canHide,
167
+ toggleVisibility,
168
+ }) as unknown as Column<unknown, unknown>;
169
+
170
+ const renderInMenu = (node: React.ReactNode) =>
171
+ render(
172
+ <DropdownMenu open={true}>
173
+ <DropdownMenuTrigger />
174
+ <DropdownMenuContent>{node}</DropdownMenuContent>
175
+ </DropdownMenu>,
176
+ );
177
+
178
+ it("renders 'Hide column' when canHide is true", () => {
179
+ renderInMenu(<HideColumn column={makeColumn()} />);
180
+ expect(screen.getByText("Hide column")).toBeInTheDocument();
181
+ });
182
+
183
+ it("returns null when getCanHide is false", () => {
184
+ renderInMenu(<HideColumn column={makeColumn({ canHide: false })} />);
185
+ expect(screen.queryByText("Hide column")).toBeNull();
186
+ });
187
+
188
+ it("calls toggleVisibility(false) on click", () => {
189
+ const toggleVisibility = vi.fn();
190
+ renderInMenu(<HideColumn column={makeColumn({ toggleVisibility })} />);
191
+ fireEvent.click(screen.getByText("Hide column"));
192
+ expect(toggleVisibility).toHaveBeenCalledWith(false);
193
+ });
194
+ });
@@ -0,0 +1,42 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { act, renderHook } from "@testing-library/react";
4
+ import { describe, expect, it } from "vitest";
5
+ import { useColumnVisibility } from "../hooks/use-column-visibility";
6
+
7
+ describe("useColumnVisibility", () => {
8
+ it("should initialize with correct default values", () => {
9
+ const { result } = renderHook(() => useColumnVisibility());
10
+ expect(result.current.columnVisibility).toEqual({});
11
+ });
12
+
13
+ it("should seed hidden columns as { name: false }", () => {
14
+ const { result } = renderHook(() => useColumnVisibility(["a", "b"]));
15
+ expect(result.current.columnVisibility).toEqual({ a: false, b: false });
16
+ });
17
+
18
+ it("should treat empty hidden list as a no-op", () => {
19
+ const { result } = renderHook(() => useColumnVisibility([]));
20
+ expect(result.current.columnVisibility).toEqual({});
21
+ });
22
+
23
+ it("should update visibility state via setter", () => {
24
+ const { result } = renderHook(() => useColumnVisibility(["a"]));
25
+
26
+ act(() => {
27
+ result.current.setColumnVisibility({ a: true, b: false });
28
+ });
29
+
30
+ expect(result.current.columnVisibility).toEqual({ a: true, b: false });
31
+ });
32
+
33
+ it("should handle functional updates", () => {
34
+ const { result } = renderHook(() => useColumnVisibility(["a"]));
35
+
36
+ act(() => {
37
+ result.current.setColumnVisibility((prev) => ({ ...prev, c: false }));
38
+ });
39
+
40
+ expect(result.current.columnVisibility).toEqual({ a: false, c: false });
41
+ });
42
+ });
@@ -71,10 +71,16 @@ export const ColumnExplorerPanel = ({
71
71
  return columnName.toLowerCase().includes(searchValue.toLowerCase());
72
72
  });
73
73
 
74
+ const rowColumnHiddenStr = prettifyRowColumnCount({
75
+ numRows: totalRows,
76
+ totalColumns,
77
+ locale,
78
+ }).rowsAndColumns;
79
+
74
80
  return (
75
81
  <div className="mb-3">
76
82
  <span className="text-xs font-semibold ml-2 flex">
77
- {prettifyRowColumnCount({ numRows: totalRows, totalColumns, locale })}
83
+ {rowColumnHiddenStr}
78
84
  <CopyClipboardIcon
79
85
  tooltip="Copy column names"
80
86
  value={columns?.map(([columnName]) => columnName).join(",\n") || ""}
@@ -17,6 +17,7 @@ import { useFilterEditor } from "./filter-editor-context";
17
17
  import { EDITABLE_FILTER_TYPES, isMembershipFilterType } from "./filters";
18
18
  import {
19
19
  ClearFilterMenuItem,
20
+ HideColumn,
20
21
  renderColumnPinning,
21
22
  renderColumnWrapping,
22
23
  renderCopyColumn,
@@ -124,6 +125,7 @@ export const DataTableColumnHeader = <TData, TValue>({
124
125
  {renderColumnPinning(column)}
125
126
  {renderColumnWrapping(column)}
126
127
  {renderFormatOptions(column, locale)}
128
+ <HideColumn column={column} />
127
129
  {canEditFilter && <DropdownMenuSeparator />}
128
130
  {canEditFilter && (
129
131
  <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;
@@ -22,7 +22,9 @@ import {
22
22
  import React, { memo } from "react";
23
23
  import { useLocale } from "react-aria";
24
24
 
25
+ import { Button } from "@/components/ui/button";
25
26
  import { Table } from "@/components/ui/table";
27
+ import { Banner } from "@/plugins/impl/common/error-banner";
26
28
  import type {
27
29
  CalculateTopKRows,
28
30
  GetRowIds,
@@ -63,6 +65,10 @@ import {
63
65
  type TooManyRows,
64
66
  } from "./types";
65
67
  import { getStableRowId } from "./utils";
68
+ import {
69
+ getUserColumnVisibilityCounts,
70
+ useColumnVisibility,
71
+ } from "./hooks/use-column-visibility";
66
72
 
67
73
  interface DataTableProps<TData> extends Partial<ExportActionProps> {
68
74
  wrapperClassName?: string;
@@ -107,6 +113,7 @@ interface DataTableProps<TData> extends Partial<ExportActionProps> {
107
113
  // Columns
108
114
  freezeColumnsLeft?: string[];
109
115
  freezeColumnsRight?: string[];
116
+ hiddenColumns?: string[];
110
117
  toggleDisplayHeader?: () => void;
111
118
  // Row viewer panel
112
119
  viewedRowIdx?: number;
@@ -158,6 +165,7 @@ const DataTableInternal = <TData,>({
158
165
  reloading,
159
166
  freezeColumnsLeft,
160
167
  freezeColumnsRight,
168
+ hiddenColumns,
161
169
  toggleDisplayHeader,
162
170
  showChartBuilder,
163
171
  isChartBuilderOpen,
@@ -176,6 +184,8 @@ const DataTableInternal = <TData,>({
176
184
  freezeColumnsLeft,
177
185
  freezeColumnsRight,
178
186
  );
187
+ const { columnVisibility, setColumnVisibility } =
188
+ useColumnVisibility(hiddenColumns);
179
189
 
180
190
  // Show loading bar only after a short delay to prevent flickering
181
191
  React.useEffect(() => {
@@ -267,6 +277,8 @@ const DataTableInternal = <TData,>({
267
277
  enableMultiCellSelection: selection === "multi-cell",
268
278
  // pinning
269
279
  onColumnPinningChange: setColumnPinning,
280
+ // col visibility
281
+ onColumnVisibilityChange: setColumnVisibility,
270
282
  // focus row
271
283
  enableFocusRow: true,
272
284
  onFocusRowChange: onViewedRowChange,
@@ -284,6 +296,7 @@ const DataTableInternal = <TData,>({
284
296
  { pagination: { pageIndex: 0, pageSize: data.length } }),
285
297
  rowSelection: rowSelection ?? {},
286
298
  cellSelection: cellSelection ?? [],
299
+ columnVisibility,
287
300
  cellStyling,
288
301
  columnPinning: columnPinning,
289
302
  cellHoverTemplate: hoverTemplate,
@@ -317,6 +330,10 @@ const DataTableInternal = <TData,>({
317
330
  [table],
318
331
  );
319
332
 
333
+ const visibilityCounts = getUserColumnVisibilityCounts(table);
334
+ const allUserColumnsHidden =
335
+ visibilityCounts.total > 0 && visibilityCounts.visible === 0;
336
+
320
337
  return (
321
338
  <FilterEditorProvider value={filterEditor}>
322
339
  <div className={cn(wrapperClassName, "flex flex-col space-y-1")}>
@@ -346,6 +363,18 @@ const DataTableInternal = <TData,>({
346
363
  downloadAs={downloadAs}
347
364
  sizeBytes={sizeBytes}
348
365
  />
366
+ {allUserColumnsHidden && (
367
+ <Banner className="mb-1 mx-2 rounded flex items-center justify-between">
368
+ <span>All columns are hidden.</span>
369
+ <Button
370
+ variant="link"
371
+ size="xs"
372
+ onClick={() => table.resetColumnVisibility(true)}
373
+ >
374
+ Unhide all
375
+ </Button>
376
+ </Banner>
377
+ )}
349
378
  <Table
350
379
  className={cn(
351
380
  "relative",
@@ -377,6 +406,7 @@ const DataTableInternal = <TData,>({
377
406
  getRowIds={getRowIds}
378
407
  showPageSizeSelector={showPageSizeSelector}
379
408
  tableLoading={reloading}
409
+ togglePanel={togglePanel}
380
410
  />
381
411
  </div>
382
412
  </CellSelectionProvider>
@@ -8,6 +8,7 @@ import {
8
8
  ArrowUpNarrowWideIcon,
9
9
  ChevronsUpDown,
10
10
  CopyIcon,
11
+ EyeOffIcon,
11
12
  FilterX,
12
13
  PinOffIcon,
13
14
  WrapTextIcon,
@@ -139,6 +140,23 @@ export function renderColumnPinning<TData, TValue>(
139
140
  );
140
141
  }
141
142
 
143
+ export function HideColumn<TData, TValue>({
144
+ column,
145
+ }: {
146
+ column: Column<TData, TValue>;
147
+ }) {
148
+ if (!column.getCanHide()) {
149
+ return null;
150
+ }
151
+
152
+ return (
153
+ <DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
154
+ <EyeOffIcon className="mo-dropdown-icon" />
155
+ Hide column
156
+ </DropdownMenuItem>
157
+ );
158
+ }
159
+
142
160
  export function renderCopyColumn<TData, TValue>(column: Column<TData, TValue>) {
143
161
  if (!column.getCanCopy?.()) {
144
162
  return null;
@@ -233,7 +251,6 @@ export function renderSorts<TData, TValue>(
233
251
  {sortDirection === "desc" && renderSortIndex()}
234
252
  </DropdownMenuItem>
235
253
  {renderClearSort()}
236
- <DropdownMenuSeparator />
237
254
  </>
238
255
  );
239
256
  }
@@ -0,0 +1,42 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ "use no memo";
3
+
4
+ import { useInternalStateWithSync } from "@/hooks/useInternalStateWithSync";
5
+ import type { Table, VisibilityState } from "@tanstack/react-table";
6
+ import { dequal as isDeepEqual } from "dequal";
7
+ import type React from "react";
8
+
9
+ interface UseColumnVisibilityResult {
10
+ columnVisibility: VisibilityState;
11
+ setColumnVisibility: React.Dispatch<React.SetStateAction<VisibilityState>>;
12
+ }
13
+
14
+ export function useColumnVisibility(
15
+ hiddenColumns?: string[],
16
+ ): UseColumnVisibilityResult {
17
+ const [columnVisibility, setColumnVisibility] =
18
+ useInternalStateWithSync<VisibilityState>(
19
+ Object.fromEntries((hiddenColumns ?? []).map((c) => [c, false])),
20
+ isDeepEqual,
21
+ );
22
+
23
+ return { columnVisibility, setColumnVisibility };
24
+ }
25
+
26
+ interface ColumnVisibilityCounts {
27
+ total: number;
28
+ visible: number;
29
+ hidden: number;
30
+ }
31
+
32
+ export function getUserColumnVisibilityCounts<TData>(
33
+ table: Table<TData>,
34
+ ): ColumnVisibilityCounts {
35
+ const userColumns = table.getAllLeafColumns().filter((c) => c.getCanHide());
36
+ const visible = userColumns.filter((c) => c.getIsVisible()).length;
37
+ return {
38
+ total: userColumns.length,
39
+ visible,
40
+ hidden: userColumns.length - visible,
41
+ };
42
+ }
@@ -443,15 +443,28 @@ export function prettifyRowCount(rowCount: number, locale: string): string {
443
443
  export const prettifyRowColumnCount = ({
444
444
  numRows,
445
445
  totalColumns,
446
+ hiddenColumns,
446
447
  locale,
447
448
  }: {
448
449
  numRows: number | "too_many";
449
450
  totalColumns: number;
451
+ hiddenColumns?: number;
450
452
  locale: string;
451
- }): string => {
453
+ }): { rowsAndColumns: string; hiddenSuffix: string | null } => {
452
454
  const rowsLabel =
453
455
  numRows === "too_many" ? "Unknown" : prettifyRowCount(numRows, locale);
454
- const columnsLabel = `${prettyNumber(totalColumns, locale)} ${new PluralWord("column").pluralize(totalColumns)}`;
455
456
 
456
- return [rowsLabel, columnsLabel].join(", ");
457
+ const hidden = hiddenColumns ?? 0;
458
+ const visibleColumns = totalColumns - hidden;
459
+
460
+ const columnsLabel =
461
+ hidden > 0
462
+ ? `${prettyNumber(visibleColumns, locale)} visible`
463
+ : `${prettyNumber(totalColumns, locale)} ${new PluralWord("column").pluralize(totalColumns)}`;
464
+
465
+ return {
466
+ rowsAndColumns: [rowsLabel, columnsLabel].join(", "),
467
+ hiddenSuffix:
468
+ hidden > 0 ? `(${prettyNumber(hidden, locale)} hidden)` : null,
469
+ };
457
470
  };
@@ -0,0 +1,50 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import type { JSX } from "react";
3
+ import { Tooltip } from "@/components/ui/tooltip";
4
+ import type { MangledSegment, UnmangledLocal } from "@/utils/local-variables";
5
+ import { CellLinkError } from "../links/cell-link";
6
+
7
+ interface Props {
8
+ local: UnmangledLocal;
9
+ }
10
+
11
+ /**
12
+ * Renders a compiler-mangled cell-local variable as the user's original name
13
+ * (e.g. `_a`) with a tooltip linking to the defining cell.
14
+ */
15
+ export const MangledLocalChip = ({ local }: Props): JSX.Element => {
16
+ const tooltipContent = (
17
+ <div className="max-w-xs">
18
+ Local variable <span className="font-code">{local.name}</span> in cell{" "}
19
+ <CellLinkError cellId={local.cellId} />.
20
+ </div>
21
+ );
22
+
23
+ return (
24
+ <Tooltip content={tooltipContent}>
25
+ <span className="font-code cursor-help">{local.name}</span>
26
+ </Tooltip>
27
+ );
28
+ };
29
+
30
+ interface MangledSegmentsProps {
31
+ segments: MangledSegment[];
32
+ }
33
+
34
+ /**
35
+ * Render an array of `splitMangledLocals` segments as alternating text and
36
+ * `<MangledLocalChip>` nodes.
37
+ */
38
+ export const MangledSegments = ({
39
+ segments,
40
+ }: MangledSegmentsProps): JSX.Element => (
41
+ <>
42
+ {segments.map((segment, idx) =>
43
+ typeof segment === "string" ? (
44
+ <span key={idx}>{segment}</span>
45
+ ) : (
46
+ <MangledLocalChip key={idx} local={segment} />
47
+ ),
48
+ )}
49
+ </>
50
+ );